2
首发地址:https://github.com/jeuino/Blo...

前言

本篇文章将介绍 JavaScript 执行上下文中,最后一个重要的属性 —— this。

this

《JavaScript 之作用域与作用域链》中,介绍了词法作用域与动态作用域,JavaScript 是基于词法作用域的,而 this 的行为却与动态作用域类似,this 的指向是基于调用栈的。

this 是在运行时进行绑定的,而不是在编写时绑定,它的指向取决于函数是怎么被引用的。

this 绑定的类型可以分为以下几类:

  • 隐式绑定
  • 显示绑定
  • new 绑定

我们直接通过例子来看 this 是如何绑定的。(多例预警,部分示例参考书籍《你不知道的JavaScript》

隐式绑定

eg1.1:

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

输出:1
原因
Image

如果你不清楚变量和函数在内存中是如何存储的,请阅读《JavaScript 之内存空间》

通过 foo() 调用函数时:解释器首先会在栈中找到 foo 函数的引用地址,然后根据引用地址到堆中找到函数并执行。这种直接在词法作用域中查找函数引用并调用的方式,this 会指向全局对象 window。

注意:在严格模式下,this 的值为 undefined。此处的严格模式是指,函数体处于严格模式下,this 的值为 undefined。否则 this 会被绑定到 Window 对象。

eg1.2:巩固例,输出结果的原因自行分析呦

function foo2() {
    console.log(this.a);
}
function foo() {
    var a = 'foo';
    foo2();
}
var a = 1;
foo(); // 1

eg2.1:

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

输出:1
原因
Image  2

通过 obj.foo() 调用函数时:解释器首先会在栈中找到 obj 对象的引用地址,然后根据引用地址到堆中找到原始对象,然后再根据 obj.foo 属性值(属性值为 foo 函数引用地址)在堆中找到函数并执行。

foo 函数是通过对象 obj 来引用的,也就是说,它的调用位置是 obj 对象,所以 this 指向 obj 对象。

eg2.2:巩固例,输出结果的原因自行分析呦

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

eg3.1:

function foo() {
    console.log(this.a);
}
var obj = {
    foo: foo,
    a: 1
};
var a = 'global';
var bar = obj.foo; 
bar(); // 'global'

输出:'global'
原因
Image  3

我想大家通过上图应该就已经明白为什么输出的是'global'了。(参考 eg1.1

eg3.2:巩固例,输出结果的原因自行分析呦

function foo() {
    console.log(this.a);
}
function doFoo(fn) {
    fn();
}
var obj = {
    foo: foo,
    a: 1
};
var a = 'global';
doFoo(obj.foo); // 'global'

总结:

隐式绑定有两种形式:

  • 直接在词法作用域中引用函数 -> this 指向 window 对象
  • 通过对象属性间接引用函数 -> this 指向这个对象

显示绑定

显示绑定 this 的几种方法:

  • call(...) 方法
  • apply(...) 方法
  • bind(...) 方法 (ES5)

call/apply 方法

举例说明:

function foo() {
    console.log(this.a);
}
var obj = {
    a: 1
};
foo.call(obj); // 1
// or
foo.apply(obj); // 1

通过 call apply 方法,可以在调用 foo 时,强制将它的 this 绑定到 obj 上。

call apply 方法的功能是一样的,它们的区别在于传递参数的格式不同。call 逐个传递单个参数(foo.call(obj,1,2,3)),apply一次性传递一个参数数组(foo.apply(obj,[1,2,3]))。

如果 call 和 apply 中传入的第一个参数是原始值(eg: 'str', true, 1),那么这个原始值会被转换成它的对象形式(也就是 new String()、new Boolean()、new Number() ..),这个过程通常被称为 “装箱” 操作。

bind 方法

使用 bind(...) 方法,会返回一个指定 this 后的函数,该函数可重复使用。

举例说明:

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

var bar = foo.bind(obj);

bar(); // 1

特殊情况

如果将 null 或者 undefined 作为 this 的绑定对象传入到 call/apply/bind 中,此时 this 指向 window。

new 绑定

使用 new 操作符来调用函数时,也就是我们经常说的发生构造函数调用时,会进行以下操作:

  • 创建一个全新的对象;
  • 这个新对象会被执行[[Prototype]]连接;
  • 这个新对象会被绑定到调用的函数的 this 上;
  • 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象;
function Foo(a) {
    this.a = a;
}

var obj = new Foo(1);

console.log(obj.a); // 1

简单来说,就是使用 new 操作符来调用函数 Foo 时,会创建一个新的对象并把它绑定到 Foo 函数执行上下文中的 this 上。

优先级

以上四条 this 绑定规则的优先级如下:

  1. new 绑定
  2. 显示绑定 call/apply/bind
  3. 隐式绑定:通过对象属性间接调用函数
  4. 隐式绑定:直接在词法作用域中调用函数,这种绑定方式也被称为默认绑定

我们再来思考一个例子:

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

(bar.foo = obj.foo)(); // 1

赋值表达式 bar.foo = obj.foo 的返回值是 obj.foo 的值,也就是目标函数 foo 的引用。因此,调用位置等价于 foo() ,所以这里应用的是默认绑定,输出结果为 1。

下一篇

在每个执行上下文中,都包括三个重要的属性:

  • 变量对象(Variable Object,VO)
  • 作用域链(Scope Chain)
  • this指向

截止到此篇文章,执行上下文中的三个属性就都介绍完啦。下篇文章将开始介绍执行上下文的生命周期,敬请期待。

参考:

JavaScript 的 this 原理
《你不知道的 JavaScript》上

Jojo
126 声望12 粉丝

Stick a little bit more every day