深入理解 javascript 中 this 的指向 ( 箭头函数 )

本文基于你不知道的 javascript 上卷和自己的理解

1. 关于 this

当一个函数被调用时,会创建一个活动记录(有时候也成为执行上下文,以后会详细讲解)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息。this 就是这个记录的一个属性。

2. this 全面解析

2.1 调用位置

首先清楚一个概念
调用位置:调用位置就是函数在代码中被调用的位置(而不是声明位置)。
例如

function foo(){ // 声明位置
    console.log("foo");
}
foo(); // 调用位置

某些编程模式会隐藏真正的调用位置。最重要的是要分析调用栈(就是为了达到当前执行位置所调用的所有函数。关于调用栈以后会和执行上下文一起讲)。

function baz(){
    // 当前调用栈是:baz
    // 因此当前调用位置是全局作用域
    console.log( "baz" );
    bar(); // <-- bar 的调用位置
}

function bar(){
    // 当前调用栈是:baz -> bar
    // 因此当前调用位置在 baz 中
    console.log( "bar" );
    foo(); // <-- foo 的调用位置
}

function foo(){
    // 当前调用栈是 baz -> bar -> foo
    // 因此当前调用位置在 bar 中
    console.log( "foo" )
}

baz(); // <-- baz 的调用位置

首先,最后一句调用了 baz() ,所以那个位置就是 baz() 的调用位置,将baz() 添加到调用栈里。然后 baz() 里面又调用了 bar(),所以 bar() 的调用位置是在这,同样将 bar() 加入到调用栈里面。接着,bar() 里面又调用了 foo(),所以 foo() 的调用位置是在 bar() 里面,将 foo() 加入到调用栈中。

明确调用位置对我们分析 this 的指向有很大的帮助。

2.2 绑定规则

2.2.1 默认绑定

默认绑定即独立的函数调用,当其他规则无法应用时的默认规则,如:

function foo(){
    console.log(this.a);
}
var a = 2 ;
foo(); // 2

调用 foo() 的时候其实相当于 window.foo(),所以 this.a 其实指向的是 window.a

2.2.2 隐式绑定

思考如下代码

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

当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文的对象。因为调用 foo()this 被绑定到 obj,因此 this.aobj.a 是一样的

对象属性引用链中只有上一层或者说最后一层在调用位置起作用。如

function foo(){
    console.log( this.a );
}
var obj1 = {
    a: 2,
    obj2: obj2
};
var obj2 = {
    a: 42,
    foo: foo
};
obj1.obj2.foo(); // 42
2.2.3 显式绑定

即使用 apply()call() 方法。它们的第一个参数是一个对象,在调用函数时将其绑定到 this。他们的主要区别就是第二个参数。
看个例子

function foo(){
    console.log( this.a );
}
var obj = {
    a: 2,
};
var bar = function(){
    foo.call( obj );
};
bar(); // 2
setTimeout(bar,100) // 2

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

我们创建了函数 bar(),并在内部调用了 foo.call(obj),因此强制把 foothis 绑定到了 obj。之后无论如何调用 bar(),它总会手动在 obj 上调用 foo。这种绑定是一种强制绑定,也成为硬绑定。

由于硬绑定是一种非常常用的模式,所以 ES5 提供了 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
2.2.4 new 绑定

使用 new 调用函数只是对函数的 ” 构造调用 “,所有的函数都可以使用 new 来调用。
使用 new 来调用函数时,会自动执行如下操作

  1. 创建(或者说构造)一个全新的对象
  2. 这个新对象会被执行 [[Prototype]] 连接
  3. 这个新对象会绑定到函数调用的 this
  4. 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象

用代码表示就是如下步骤

function foo(a){
    this.a = a;
}
var bar = new foo(2);
console.log(bar.a); // 2
// 其中 new foo(2) 进行的是类似如下的操作
{
    var obj = new Object();
    obj.__proto__ = foo.prototype;
    var result = foo.call(obj,"2");
    return result === 'object' ? result : obj
}

使用 new 来调用 foo( … ) 时,我们会构造一个新对象并把它绑定到 foo( … ) 调用中的 this

2.3 优先级

毫无疑问,默认绑定优先级最低
接下来,我们看看其他绑定的优先级

function foo(a){
    console.log( this.a );
}
var obj1 = {
    a: 2,
    foo: foo
};
var obj2 = {
    a: 3,
    foo: foo
};
obj1.foo(); // 2
obj2.foo(); // 3
obj1.foo.call(obj2); // 3
obj2.foo.call(obj1); // 2
// 显然,显式绑定优先级更高
function foo(something){
    this.a = something;
}
var obj1 = {
    foo:foo
};
var obj2 = {};
obj1.foo( 2 );
console.log( obj1.a ); // 2
obj1.foo.call( obj2, 3 );
console.log( obj2.a ); // 3
var bar = new obj1.foo( 4 );
console.log( obj1.a ); // 2
console.log( bar.a ); // 4
// 可以看到,new 绑定比隐式绑定优先级高
function foo(something){
    this.a = something;
}
var obj1 = {};
var bar = foo.bind( obj1 );
bar( 2 );
console.log( obj1.a ); // 2
var baz = new bar(3); 
console.log( obj1.a ); // 2
console.log( baz.a ); // 3
判断 this
  1. 函数是否在 new 中调用(new 绑定)?如果是的话 this 绑定的是新创建的对象
    var bar = new foo()
  2. 函数是否通过 call、apply(显式绑定)或者硬绑定调用(bind)?如果是的话,this 绑定的是指定的对象
    var bar = foo.call()
  3. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的haul,this 绑定的是哪个上下文对象
    var bar = obj1.foo()
  4. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined,否则绑定到全局对象。
    var bar = foo()

2.4 箭头函数

箭头函数不适用 this 的四种标准规则,而是根据外层作用域来决定
来看下面这个例子

function foo(){
    // 返回一个箭头函数
    return (a)=>{
        // this 继承自 foo()
        console.log( this.a );
    };
}
var obj1 = {
    a:2
};
var obj2 = {
    a:3
};
var bar = foo.call( obj1 );// 调用位置
bar.call( obj2 ); // 2,不是 3!

foo() 内部创建的箭头函数会捕获调用 foo() 时的 this。由于 foo()this 绑定到 obj1barthis 也会绑定到 obj1,箭头函数的绑定无法修改。(new 绑定也不行!)

3. 总结

  1. 箭头函数的 this 绑定看的是 this 所在的函数定义在哪个对象下,绑定到哪个对象则 this 就指向哪个对象

  2. 如果有对象嵌套的情况,则 this 绑定到最近的一层对象上

猜你喜欢

转载自blog.csdn.net/zhang6223284/article/details/81288667