彻底理解JS的this绑定

258 阅读7分钟

定义理解

当一个函数被调用时,会创建执行上下文,执行上下文会包含函数的调用栈,调用方法,传入的参数等信息,this就是执行上下文的其中的一个属性,是一个动态绑定的指针,是在函数被调用时发生的绑定

this的绑定分为以下5种

  • 默认绑定
  • 隐式绑定
  • 显式绑定
  • new绑定
  • 箭头函数绑定

默认绑定

  • 非严格模式下:独立函数调用,无法应用其他绑定规则的默认规则,this指向全局对象
  • 严格模式下: 不能将全局对象应用于默认绑定,this会绑定到undefined,但是在严格模式下调用函数则不影响默认绑定

独立调用foo函数,this应用默认绑定,绑定到了window上

var x = 10;
function foo(){
    console.log(this.x) // 10
}

foo();

运行在严格模式下,this会绑定到undefined

var x = 2;
function foo() { // 运行在严格模式下,this会绑定到undefined
    "use strict";
    
    console.log(this.x);
}

// 调用
foo(); // TypeError: Cannot read property 'a' of undefined

严格模式下调用函数则不影响默认绑定

var x = 2;
function foo() { // 运行
    console.log(this.x);
}

(function() {
    "use strict";
    
    foo(); // 2
})();

隐式绑定

当函数引用有上下文对象时,会把函数中的this绑定到这个上下文对象。对象属性引用链中,只有最后一层在调用中起作用

调用位置的上下文对象是obj,此时应用隐式绑定,this绑定到obj上,所以this.a的值为2

var obj = {
    a: 2,
    foo: foo
};
function foo() {
    console.log( this.a );
}

obj.foo() // 2

隐式丢失:此时foo不是作为obj的对象被调用,而是将obj.foo赋值给了bar,bar引用的是函数foo本身,所以独立调用的时候,foo函数中的this就会绑定到当前的执行上下文对象上,也就是window

var obj = {
    a: 2,
    foo: foo
};
function foo() {
    console.log( this.a );
}

var bar = obj.foo; // 函数别名
bar() // undefined

在实际开发中,将方法作为回调传给子组件,是很常用的方法,参数传递就是一种隐式赋值,所以在回调函数中如果使用了this,那么如果不把回调函数绑定当前的this,就会出现隐式丢失

实际应用中的隐式丢失举例

class MyComponent extends React.Component {
    this.a = 2;
    
    // 这样的写法如果在方法中调用了this,传递给组件作为回调函数,
    // 就会出现隐式丢失,因为函数传递下去,是保持的对函数的引用
    testFunc(){
        console.log(this.a)
    }
    
    render(){
        return (
        <div>
         <Button onClick={this.testFunc}>点击事件</Button>
        </div>
        )
    }
}

所以才会有两种方法来避免隐式丢失的问题

class MyComponent extends React.Component {
    constructor(props) {
        super(props);
        this.state = {};
        // 方法1:通过bind强制绑定当前的this
        this.testFunc = this.testFunc.bind(this);
  }
    
    // 方法2:通过箭头函数绑定当前的this
    testFunc = ()=> {
        console.log(this.a)
    }
    
    render(){
        return (
        <div>
         <Button onClick={this.testFunc}>点击事件</Button>
        </div>
        )
    }
}

注意

class Foo {
  constructor(name){
    this.name = name
  }
  display(){
    console.log(this.name);
  }
}
var foo = new Foo('Saurabh');
foo.display(); // Saurabh

//下面的赋值操作模拟了上下文的丢失。 
//与 实际在 React Component 中将处理程序作为 callback 参数传递 相似。

var display = foo.display; 
display();              // TypeError: this is undefined

按照默认绑定的规则,在非严格模式运行代码,this 的值会指向全局对象,在严格模式 this 是 undefined。

类声明和类表达式的主体以 「严格模式」 执行,即构造函数、静态方法和原型方法。Getter 和 setter 函数也在严格模式下执行。

而 import 的模块,默认也是用「严格模式」

显示绑定

通过call(...)和apply(...)方法。第一个参数是一个对象,在调用函数时将这个对象绑定到this,call和apply的区别就是传入的其他参数不同,另外第一个参数也是可选的

call除了第一个参数以外,可以接收一个参数列表,apply只接受一个参数数组

function foo(name,age) {
    console.log(name);
    console.log(age);
    console.log(this.value);
}

let obj = {
    value: 'value'
};

foo.call(obj,'sam',666); //  调用foo时强制把foo的this绑定到obj上
foo.apply(obj,['sam',666]); //  调用foo时强制把foo的this绑定到obj上

如果传入了原始值来当做this的绑定对象,这个原始值会转化成它的对象形式,也就是new String()、new Boolean()、new Numbe(),通常被称为装箱

另外 如果把 null 或 undefined 作为this的绑定对象传入 call、apply、bind ,这些值在调用时会忽略,实际应用的是默认绑定规则

硬绑定

类似借用构造函数

function foo() {
    console.log( this.a );
}

var obj = {
    a: 2
};

var bar = function() {
    foo.call( obj );
};

bar(); // 2

// 硬绑定的bar不可能再修改它的this
bar.call( window ); // 2

call和apply不能解决绑定丢失的问题,所以和call和apply类似的方法还有个bind,bind传入的第一个参数也是需要绑定的对象,只是该方法是返回一个函数,这个方法是内置在ES5上的Function.prototype.bind中,bind会返回一个硬绑定的函数

function foo(something) {
    console.log( this.a, something );
    return this.a + something;
}

var obj = {
    a: 2
};

var bar = foo.bind( obj );

var b = bar( 3 ); // 2 3
console.log( b ); // 5

api绑定的上下文,JS中也有很多内置函数,会提供一个可选参数,就是传入当前的上下文对象,用于确保回调函数绑定指定的this,实际上就是通过call和apply实现的绑定

function foo(el) {
	console.log( el, this.id );
}

var obj = {
    id: "awesome"
}

var myArray = [1, 2, 3]
// 调用foo(..)时把this绑定到obj
myArray.forEach( foo, obj );
// 1 awesome 2 awesome 3 awesome

new绑定

  1. 在JS中,构造函数只是使用new操作符被调用时的普通函数,JS没有传统意义上类的概念
  2. 包括内置对象函数在内的所有函数都可以用new来调用,被称为构造函数调用
  3. 理论上是实际上不存在“构造函数”,只存在函数的“构造调用”,只是我们习惯以面向对象的构造函数来称呼这种调用方式

使用new的过程 1、创建一个新对象 2、对这个对象执行原型连接 3、新对象绑定到函数调用的this 4、如果没有返回其他对象,则自动返回这个创建的新对象

手写模拟new的调用

function myNew(){
    // 取出传入的构造函数
    const Con = [].shift.call(arguments)
    // 创建一个新的对象,并设置对象原型,利用Object.create可以直接实现
    let obj = Object.create(Con.prototype);
    // 进行this绑定,传入剩余参数
    let result = Con.apply(obj,arguments);
    // 如果函数有返回对象,则返回函数的对象,如果没有则返回new方法内构造的对象
    return result instanceof Object ? result : obj;
}

// 实际使用
function Parent(){}
// 原始的new
const obj = new Parent(...);
// 自己模拟的new调用
const obj2 = myNew(Parent,...)

这里有的地方的写法是直接通过创建一个obj,然后修改obj.__proto__来实现的,ES5新增了Object.create方法,可以让我们在新增对象的时候,同时指定新增对象的原型,这里就应该用这个方法实现,因为__proto__的标准化是在ES6实现的,而且并不推荐直接访问,如果要获取或者修改原型,有Object.getPrototypeOf()Object.setPrototypeOf()可以实现

箭头函数

箭头函数不应用上述任何规则,而是根据最近的非箭头函数的当前作用域来决定this,箭头函数没有自己的this,所以必须根据作用域链往外层查找,直到找到了一个绑定了this的函数作用域,并指向该函数作用域的this

箭头函数绑定中,this指向外层作用域,并不一定是第一层,也不一定是第二层。 因为没有自身的this,所以只能根据作用域链往上层查找,直到找到一个绑定了this的函数作用域,并指向调用该普通函数的对象。

使用箭头函数和function函数的区别

  • 箭头函数,方法会声明在每个类的实例上
  • function函数,方法会声明在class的原型上,class的实例可以在原型上找到这个方法并复用
  • 箭头函数没有arguments对象,不可以使用yield命令,箭头函数不能用作Generator函数
  • 不可以使用new调用,因为没有自己的this,无法使用call,apply,没有prototype属性

如果在constructor中使用bind绑定函数,则还是会在每个实例上创建新的函数 总的说来,箭头函数和非箭头函数,确实会有一些性能差异,不考虑this指向的情况下,可以在原型上得到这个函数的声明,但是在考虑使用this的情况下,在constructor中的bind声明,和使用箭头函数没有什么大的区别,但是使用箭头函数,简介轻便的语法,可以减少编码人员的思考负担

this绑定的优先级

  1. new绑定
  2. 显式绑定
  3. 隐式绑定
  4. 默认绑定

参考1

参考2

参考3