浅析 Javascript 中 this 的指向 ( 箭头函数 )

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

1. 关于 this

当一个函数被调用时,会创建一个活动记录(有时候也成为执行上下文,见 浅析 javascript 中执行环境,变量对象及作用域链)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息。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 是一样的

对象属性引用链中只有上一层或者说最后一层在调用位置起作用(这里是由于作用域链对于 this 的寻找只会到当前的活动对象或变量对象中,不会到上一层)。如

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. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上下文对象
    var bar = obj1.foo()
  4. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined,否则绑定到全局对象。
    var bar = foo()

2.4 箭头函数

箭头函数不适用 this 的四种标准规则,而是在 this 定义的时候保存当前的作用域链,然后顺着当前的作用域链寻找 this,并且只会在作用域链最前端的活动对象或变量对象中寻找(有不理解的可以参考 浅析 javascript 中执行环境,变量对象及作用域链)。简单来说就是箭头函数的 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 绑定也不行!)

ES6 标准入门里面对箭头函数 this 的指向有如下说法:

函数体内的 this 对象就是定义时所在的对象,而不是调用时所在的对象。

3. 总结

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

  2. 一般情况下 this 的绑定是默认绑定,如果有 new 绑定则 new 绑定优先级最高,其次是显式绑定,然后再是隐式绑定。如果有对象嵌套的情况,则 this 绑定到最近的一层对象上

  • 9
    点赞
  • 61
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值