likes
comments
collection
share

深入JavaScript之this

作者站长头像
站长
· 阅读数 14

前言

在学习JavaScript的过程中,this是一个不可忽视的概念,很多巧妙的操作都是基于this实现的。但this在不同环境下的取值往往是一个令人头疼的问题。

this是如何定义的

先看一下官方文档中对this的说明。

ECMAScript标准中this的解释

深入JavaScript之this深入JavaScript之this

The abstract operation ResolveThisBinding determines the binding of the keyword this using the LexicalEnvironment of the running execution context

大致意思是,this关键字是一个运行时的语义,指向活跃状态的执行上下文的词法环境(LexicalEnvironment)

MDN中this的定义

当前执行上下文(global、function 或 eval)的一个属性,在非严格模式下,总是指向一个对象,在严格模式下可以是任意值。

小结

上面的两种关于this的解释都和上下文有关,在JavaScript中,一段代码在执行前会生成一个活动对象,作为代码的执行环境,里面存储了代码段中声明的变量,函数等,我们也将活动对象称做执行上下文。 除了变量,函数外,在执行上下文中还会确定this的值,也就是this绑定。在this绑定过程中,this一般指向代码运行时所在的环境(LexicalEnvironment)。代码执行过程中获取的this都是从执行上下文中取值的。

this有什么作用

万物皆有因,在JavaScript这门编程语言中this被创建出来一定有存在的意义,能够发挥一定的作用。this设计的目的就是在函数体内部,指代当前函数的运行环境。因此我们可以通过this在函数体内部便捷的引用运行环境中的变量而不需要传参,极大的方便了开发。

我们通过一个简单的例子来体验一下this的效果。

var person = {
    name: '小明',
    age: 18,
    hobby: '篮球',
    playGame: function(){
      // 待补充
    }
}

现在有一个对象person,有name,age,hobby三个属性,还有一个playGame方法,现在我们的需求是:

调用person.playGame()方法,输出:我叫小明,今年18岁,我的爱好是篮球!

如果没有this,我们该如何实现这个操作?直接的思路就是传入需要的数据,然后输出。

var person = {
    name: '小明',
    age: 18,
    hobby: '篮球',
    playGame: function(name,age,hobby){
      console.log(`我叫${name},今年${age}岁,我的爱好是${hobby}!`)
    }
}
person.playGame(person.name,person.age,person.hobby);

再简化一下,直接传入person对象,得到的代码为:

var person = {
    name: '小明',
    age: 18,
    hobby: '篮球',
    playGame: function(self){
      console.log(`我叫${self.name},今年${self.age}岁,我的爱好是${self.hobby}!`)
    }
}
person.playGame(person);

如果是使用this来实现这个需求,则代码为:

var person = {
    name: '小明',
    age: 18,
    hobby: '篮球',
    playGame: function(){
      console.log(`我叫${this.name},今年${this.age}岁,我的爱好是${this.hobby}!`)
    }
}
person.playGame();

两者对比发现,区别不是很大,只不过person是显示传参,需要我们手动设置,存储在函数的参数列表里面

而this是隐式的,JavsScript直接帮我们设置了,存储在代码执行前生成的上下文中。通过this,我们可以在函数内部获取当前函数的运行环境,使用环境中的一些变量。这也是this最初被设计的目的。

再看一个经典的例子。

var f = function () {
  console.log(this.x);
}

var x = 1;
var obj = {
  f: f,
  x: 2,
};

// 单独执行
f() // 1

// obj 环境执行
obj.f() // 2

同一个函数,在不同的环境中执行,它的this指向是不同的,它总是指向当前函数的运行环境

判断this的指向

this一般在函数中使用,但是在不同的函数中,this指向也是有所区别的。

标准函数

在标准函数中,this引用的是把函数当成方法调用的上下文对象,这时通常称其为this值。

在全局上下文中调用函数时,this指向window

var foo = 123;
function print(){
	this.foo = 234;
    console.log(this); // window
	console.log(foo); // 234
}
print();	//等价于 window.print()

在全局上下文中调用,等价于window对象在调用这个函数,此时函数的this指向的是window对象。

以某个对象的方法调用时,this指向这个对象

var color = 'red';
let o = {
    color: 'blue',
};

function sayColor() {
    console.log(this.color);
}

sayColor();   //'red'

o.sayColor = sayColor;
o.sayColor();  //'blue'

小结

其实上面两种情况,都可以看做是同一种情况,即把函数当做方法调用。只不过在全局调用时,window对象是可以省略的。 从上面的例子可以看出,同一个函数在不同的地方调用,其this的指向是不同的。也就是说,this的指向是在函数调用时确定的,和函数在哪个作用域创建无关

做几道例题测试下吧

1,let,const
let a = 1;
const b = 2;
var c = 3;
function print() {
    console.log(this.a);  //undefined
    console.log(this.b);  //undefined
    console.log(this.c);  //3
}
print();
console.log(this.a);  //undefined

let/const定义的变量存在暂时性死区,而且不会挂载到window对象上,因此print中是无法获取到a和b的。

2,对象内执行
var a = 1;
function foo() {
    console.log(this.a); 
}
const obj = {
    a: 10,
    bar() {
        foo(); // 1
    }
}
obj.bar(); 

foo虽然在obj的bar函数中,但foo函数仍然是独立运行的,foo中的this依旧指向window对象。

3,函数内执行
var a = 1
function outer() {
    var a = 2
    function inner() {
        console.log(this.a) // 1
        console.log(a);   //2
    }
    inner()
}
outer()

查找标准函数中某个变量的值时,可以通过作用域链寻找,但寻找标准函数中的this值时,需要判断它被哪个对象所调用。inner()正常执行,不是作为对象的某个方法调用,this指向Window

4,自执行函数
var a = 1;
(function(){
    console.log(this);
    console.log(this.a)
}())
function bar() {
    b = 2;
    (function(){
        console.log(this);
        console.log(this.b)
    }())
}
bar();

默认情况下,自执行函数的this指向window 自执行函数只要执行到就会运行,并且只会运行一次,this指向window。

箭头函数

箭头函数的this

箭头函数没有this,它的this指向的是定义箭头函数的作用域中对应的上下文

var color = 'red';
let o = {
    color: 'blue',
};

let sayColor = () => console.log(this.color)

sayColor();   //'red'

o.sayColor = sayColor;
o.sayColor();  //'red'

上述代码中,箭头函数是在全局作用域中定义的,也就是说this会指向全局作用域。由于JS采用的是静态作用域,作用域是在代码创建时就确定了的,所以,箭头函数中的this是指向定义箭头函数的作用域中执行上下文的。

如果是在全局中定义箭头函数,箭头函数中的this指向全局上下文,全局上下文中的this是指向window。

如果是在标准函数中定义箭头函数,那么箭头函数的this指向标准函数上下文中的this。标准函数的this是根据函数执行的位置确定的

做几道例题测试下

1 在对象方法中使用箭头函数
var name = 'tom'
const obj = {
    name: 'zc',
    intro: () => {
        console.log('My name is ' + this.name)
    }
}
obj.intro()  //My name is tom

箭头函数在全局作用域中创建,this指向全局上下文。

2 箭头函数与普通函数比较
var name = 'tom';
const obj = {
    name: 'zc',
    intro: function () {
        return () => {
            console.log('My name is ' + this.name)
        }
    },
    intro2: function () {
        return function () {
            console.log('My name is ' + this.name)
        }
    }
}
obj.intro2()()  //My name is tom
obj.intro()()   //My name is zc

分析:调用intro方法返回值是一个箭头函数,箭头函数中的this是取定义箭头函数作用域中的this,也就是intro函数的作用域。intro函数是一个标准函数,它的this根据调用位置确定,obj调用的intro方法,所以intro中的this指向obj。obj.intro()() //My name is zc

intro2方法返回值是一个标准函数,它的this是根据调用位置来确定的。其在全局中调用,所以this指向window

obj.intro2()()  //My name is tom

//等价于
let fn = obj.intro2();  
fn()   //My name is tom    全局中调用

new 构造函数

构造函数中this的指向

如果函数是使用new调用,函数执行前会新创建一个对象,this指向这个创建的对象。

function User(name, age) {
    this.name = name;
    this.age = age;
}
var name = 'Tom';
var age = 18;

var zc = new User('zc', 24);
console.log(zc.name) // 'zc'

new 操作符实现的操作

  1. 创建一个对象。
  2. 将对象的原型对象设置为构造函数的原型对象,来继承原型对象上的属性和方法。
  3. 将构造函数中的this指向该对象,为这个对象添加属性和方法。
  4. 返回这个对象。

new 操作符手写实现

function createNew(con,...args){
  //以构造函数原型对象为原型对象创建一个对象
  let obj = Object.create(con.prototype);
  //将构造函数中的this指向这个对象
  const result = con.call(obj,...args);
  // 返回创建的对象
  return result instanceof Object? result:obj;
}

//测试
function Person(name,age){
  this.name = name;
  this.age = age;
}

let person1 = new Person('张三',25);
console.log(person1);
let person2 = createNew(Person,'李四',26);
console.log(person2);

控制台输出结果:深入JavaScript之this

更改this的指向

函数中this的值是JavaScript代码在执行上下文中自动设置的,但JavaScript也提供了callapplybind等方法让我们手动设置this的值。

call方法

方法说明

call方法使用一个指定的this值和单独给出的一个或多个参数调用一个函数。返回调用函数的返回值。

它将调用函数中this指向改成指向call方法中传入的第一个参数。语法格式:function.call(thisArg, arg1, arg2, ...)。示例:

var name = "honny";

function sayHello(name,emotion){
  console.log(`hello ${this.name},My name is ${name},I'm very ${emotion} to see you`);
}

//全局调用sayHello方法,this指向window
sayHello('Jock','happy');
//hello honny,My name is Jock,I'm very happy to see you

let person = {
  name:'nack'
}

//使用call方法,将sayHello函数中的this指向person
sayHello.call(person,'Jock','happy');
//hello nack,My name is Jock,I'm very happy to see you

手写call方法

//手写call方法
function myCall(target,...args){
    //获取目标对象
    let obj = target||window;
    //创建唯一标识
    let symbolName = Symbol();
  
    //获取方法:此时是需要执行的函数调用call方法,this指向执行的函数
    //function.call()  this->function
    //在目标对象上添加这个函数
    obj[symbolName] = this;
  
    //执行目标对象上添加的函数 并获取返回值
    //此次调用中,函数是在目标对象中调用的,所以函数中的this指向目标对象
    let result = obj[symbolName](...args);
  
    //调用完毕后删除目标对象上添加的函数
    delete obj[symbolName];
  
    //返回函数调用结果
    return result;
}

//测试
var name = "honny";

function sayHello(name,emotion){
  console.log(`hello ${this.name},My name is ${name},I'm very ${emotion} to see you`);
}

let person = {
  name:'nack'
}

//在函数原型上添加myCall方法
Function.prototype.myCall = myCall;
//调用myCall方法
sayHello.myCall(person,'Jock','happy');
// hello nack,My name is Jock,I'm very happy to see you

apply方法

方法说明

apply调用一个具有给定值this的函数,以及以一个数组或类数组对象的形式提供的参数,返回调用函数的返回值。同call方法类似,它也能够将调用函数中的this的指向改变为apply方法传入的第一个参数。不过于call方法不同的是,call方法接收一个参数列表,但apply方法接收一个单数组。

语法格式:function.apply(thisArg,[...arg])

示例:

var name = "honny";

function sayHello(name,emotion){
  console.log(`hello ${this.name},My name is ${name},I'm very ${emotion} to see you`);
}

sayHello('Jock','happy');
//hello honny,My name is Jock,I'm very happy to see you

let person = {
  name:'nack'
}
sayHello.apply(person,['Jock','happy']);
//hello nack,My name is Jock,I'm very happy to see you

注意:apply以数组的方式接收调用函数的参数,但在调用函数中还是以参数列表的形式接收的

手写apply方法

//手写apply
function myApply(target,argsArray){
  //1 获取目标对象
  let obj = target || window;
  //2 创建唯一编码
  let symbol = Symbol();
  //3 在目标对象上添加函数
  obj[symbol] = this;
  //4 执行函数获取结果
  let result = obj[symbol](...argsArray);
  //5 执行完毕,删除函数
  delete obj[symbol];
  //6 返回执行结果
  return result;
}

//测试
var name = "honny";
function sayHello(name,emotion){
  console.log(`hello ${this.name},My name is ${name},I'm very ${emotion} to see you`);
}
let person = {
  name:'nack'
}
Function.prototype.myApply = myApply;
sayHello.myApply(person,['Jock','happy']);
//hello nack,My name is Jock,I'm very happy to see you

bind方法

方法说明

bind方法创建一个新的函数,在bind被调用时,这个新函数的this值被指定为bind方法的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

与call和apply相同的是,bind方法能够将调用函数的this指向bind的第一个参数。但不同的是bind并不会执行这个函数,而是返回更改this后的新函数

语法格式:function.bind(thisArg, arg1, arg2, ...)

示例:

var name = "honny";

function sayHello(name,emotion){
  console.log(`hello ${this.name},My name is ${name},I'm very ${emotion} to see you`);
}

sayHello('Jock','happy');
//hello honny,My name is Jock,I'm very happy to see you

let person = {
  name:'nack'
}

//情况一
let sayHelloAfterBind = sayHello.bind(person,'Jock','happy');
sayHelloAfterBind();
//hello nack,My name is Jock,I'm very happy to see you

//情况二
let sayHelloAfterBind = sayHello.bind(person);
sayHelloAfterBind('Jock','happy');
//hello nack,My name is Jock,I'm very happy to see you

//情况三
let sayHelloAfterBind = sayHello.bind(person,'Jock');
sayHelloAfterBind('happy');
//执行方法时接收的实际参数为:'Jock','happy'
//hello nack,My name is Jock,I'm very happy to see you

注意:由于调用bind返回的是一个函数,函数是可以传参数的,函数执行使用的参数是bind第一个参数之后的参数加上调用函数时传入的参数

手写bind方法

//手写 bind
function myBind(target,...args1){
  //1 获取目标对象
  let obj = target || window;
  //2 创建唯一编码
  const symbol = Symbol();
  //3 再目标对象上添加函数
  obj[symbol] = this;
  //4 返回一个函数
  return function(...args2){
    return obj[symbol](...args1,...args2);
  }
}

//测试
var name = "honny";

function sayHello(name,emotion){
  console.log(`hello ${this.name},My name is ${name},I'm very ${emotion} to see you`);
}

let person = {
  name:'nack'
}

Function.prototype.myBind = myBind;
    
let sayHelloMyBind = sayHello.myBind(person,'Jock');
sayHelloMyBind('happy');
//hello nack,My name is Jock,I'm very happy to see you

参考资料