你不知道的Javascript:this

this是什么?

JavaScript 中最令人困惑的机制之一就是 this 关键字。它是一个在每个函数作用域中自动定义的特殊标识符关键字,但即便是一些老练的 JavaScript 开发者也对它到底指向什么感到困扰。

我们要明白,this 不是编写时绑定,而是运行时绑定。它依赖于函数调用的上下文条件。this 绑定与函数声明的位置没有任何关系,而与函数被调用的方式紧密相连。

那该如何来判断函数的执行是如何绑定的this?上面已经说名,this与函数被调用的方式紧密相连,那这个被调用的方式就是一个判断的调用点。

调用点(Call-site)

调用点:函数在代码中被调用的位置(而不是被声明的位置)。一般来说寻找调用点就是:“找到一个函数是在哪里被调用的”,但它不总是那么简单,比如某些特定的编码模式会使 真正的 调用点变得不那么明确。

考虑 调用栈(call-stack) (使我们到达当前执行位置而被调用的所有方法的堆栈)是十分重要的。我们关心的调用点就位于当前执行中的函数 之前 的调用。

以下代码展示调用栈和调用点

function test1() {
    // 调用栈是: `test1`
    // 我们的调用点是 global scope(全局作用域)

    console.log( "test1" );
    test2(); // <-- `test2` 的调用点
}

function test2() {
    // 调用栈是: `test1` -> `test2`
    // 我们的调用点位于 `test1`

    console.log( "test2" );
    test3(); // <-- `test3` 的 call-site
}

function test3() {
    // 调用栈是: `test1` -> `test2` -> `test3`
    // 我们的调用点位于 `test2`

    console.log( "test3" );
}

test1(); // <-- `test1` 的调用点

从代码中分析寻找调用点时要小心,因为它是影响 this 绑定的唯一因素。

默认绑定(Default Binding)

第一种规则最常见的就是:独立函数调用。可以认为这种 this 规则是在没有其他规则适用时的默认规则。

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

var a = 2;

foo(); // 2

第一点: 在全局作用域中的声明变量var a = 2,是全局对象的同名属性的同义词。它们不是互相拷贝对方,它们就是彼此。

第二点: 我们看到当foo()被调用时,this.a 解析为我们的全局变量 a。为什么?因为在这种情况下,对此方法调用的 this 实施了 默认绑定,所以使 this 指向了全局对象。

我们怎么知道这里适用 默认绑定 ?我们考察调用点来看看 foo() 是如何被调用的。在我们的代码段中,foo() 是被一个直白的,毫无修饰的函数引用调用的。没有其他的我们将要展示的规则适用于这里,所以 默认绑定 在这里适用。

如果 strict mode 在这里生效,那么对于默认绑定来说全局对象是不合法的,所以 this 将被设置为 undefined

function foo() {
    "use strict";

    console.log( this.a );
}

var a = 2;

foo(); // TypeError: `this` is `undefined`

隐含绑定(Implicit Binding)

另一种规则是:调用点是否有一个环境对象(context object),也称为拥有者(owning)或容器(containing)对象。

考虑这段代码:

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

var obj = {
    a: 2,
    foo: foo
};

obj.foo(); // 2

注意: foo() 被声明然后作为引用属性添加到 obj 上的方式。无论 foo() 是否一开始就在 obj 上被声明,还是后来作为引用添加(如上面代码所示),这个 函数 都不被 obj 所真正“拥有”或“包含”。

然而,调用点 使用 obj 环境来 引用 函数,所以你 可以说 obj 对象在函数被调用的时间点上“拥有”或“包含”这个 函数引用

不论你怎样称呼这个模式,在 foo() 被调用的位置上,它都是一个指向 obj 的对象引用。当一个方法引用存在一个环境对象时,隐含绑定 规则会说:是这个对象应当被用于这个函数调用的 this 绑定。

因为objfoo() 调用的 this,所以 this.a 就是 obj.a 的同义词。

只有对象属性引用链的最后一层是影响调用点的。比如:

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

var obj2 = {
    a: 42,
    foo: foo
};

var obj1 = {
    a: 2,
    obj2: obj2
};

obj1.obj2.foo(); // 42
隐含丢失(Implicitly Lost)

this 绑定最常让人困惑的事情之一,就是当一个 隐含绑定 丢失了它的绑定,这通常意味着它会退回到 默认绑定, 根据 strict mode 的状态,其结果不是全局对象就是undefined

考虑这段代码:

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

var obj = {
    a: 2,
    foo: foo
};

var bar = obj.foo; // 函数引用!

var a = "oops, global"; // `a` 也是一个全局对象的属性

bar(); // "oops, global"

调用点是 bar(),一个直白,毫无修饰的调用,因此 默认绑定 适用于这里。

当我们考虑传递一个回调函数时,会怎样了?

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

var obj = {
    a: 2,
    foo: foo
};

var a = "oops, global"; // `a` 也是一个全局对象的属性

setTimeout( obj.foo, 100 ); // "oops, global"

没有区别,同样的结果。

正如我们刚刚看到的,我们的回调函数丢掉他们的 this 绑定是十分常见的事情。

不管哪一种意外改变this 的方式,你都不能真正地控制你的回调函数引用将如何被执行,所以你(还)没有办法控制调用点给你一个故意的绑定。我们很快就会看到一个方法,通过 固定 this 来解决这个问题。

明确绑定(Explicit Binding)

如果我们想强制一个函数调用使用某个特定对象作为 this 绑定,而不在这个对象上放置一个函数引用属性呢?

函数都有一些工具: 拥有call(..)apply(..) 方法。它们接收的第一个参数都是一个用于 this 的对象,之后使用这个指定的 this 来调用函数。因为你已经直接指明你想让 this 是什么,所以我们称这种方式为 明确绑定(explicit binding)

考虑这段代码:

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

var obj = {
    a: 2
};

foo.call( obj ); // 2

通过 foo.call(..) 使用 明确绑定 来调用 foo,允许我们强制函数的 this 指向 obj

注意:this 绑定的角度讲,call(..)apply(..) 是完全一样的。它们确实在处理其他参数上的方式不同,但那不是我们当前关心的。

new 绑定(new Binding)

第四种也是最后一种 this 绑定规则,要求我们重新思考 JavaScript 中关于函数和对象的常见误解。

在传统的面向类语言中,“构造器”是附着在类上的一种特殊方法,当使用 new 操作符来初始化一个类时,这个类的构造器就会被调用。通常看起来像这样:

something = new MyClass(..);

JavaScript 拥有 new 操作符,而且使用它的代码模式看起来和我们在面向类语言中看到的基本一样;大多数开发者猜测 JavaScript 机制在做某种相似的事情。但是,实际上 JavaScript 的机制和 new 在 JS 中的用法所暗示的面向类的功能 没有任何联系。

首先,让我们重新定义 JavaScript 的“构造器”是什么。在 JS 中,构造器 仅仅是一个函数,它们偶然地与前置的 new 操作符一起调用。它们不依附于类,它们也不初始化一个类。它们甚至不是一种特殊的函数类型。它们本质上只是一般的函数,在被使用 new 来调用时改变了行为。

当在函数前面被加入 new 调用时,也就是构造器调用时,下面这些事情会自动完成:

  1. 一个全新的对象会凭空创建(就是被构建)
  2. 这个新构建的对象会被接入原形链([[Prototype]]-linked)
  3. 这个新构建的对象被设置为函数调用的 this 绑定
  4. 除非函数返回一个它自己的其他对象,否则这个被 new 调用的函数将 自动 返回这个新构建的对象。

考虑这段代码:

function foo(a) {
    this.a = a;
}

var bar = new foo( 2 );
console.log( bar.a ); // 2

通过在前面使用 new 来调用 foo(..),我们构建了一个新的对象并把这个新对象作为 foo(..) 调用的 thisnew 是函数调用可以绑定 this 的最后一种方式,我们称之为 new 绑定(new binding)


一切皆有顺序

如此,我们已经揭示了函数调用中的四种 this 绑定规则。你需要做的 一切 就是找到调用点然后考察哪一种规则适用于它。但是,如果调用点上有多种规则都适用呢?这些规则一定有一个优先顺序,我们下面就来展示这些规则以什么样的优先顺序实施。

很显然,默认绑定 在四种规则中优先权最低的。所以我们先把它放在一边。

隐含绑定明确绑定 哪一个更优先呢?我们来测试一下:

function foo() {
    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

所以, 明确绑定 的优先权要高于 隐含绑定,这意味着你应当在考察 隐含绑定 之前 首先 考察 明确绑定 是否适用。

现在,我们只需要搞清楚 new 绑定 的优先级位于何处。

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 绑定 的优先级要高于 隐含绑定。那么你觉得 new 绑定 的优先级较之于 明确绑定 是高还是低呢?

注意newcall/apply 不能同时使用,所以 new foo.call(obj1) 是不允许的,也就是不能直接对比测试 new 绑定明确绑定。但是我们依然可以使用 硬绑定 来测试这两个规则的优先级。

在我们进入代码中探索之前,回想一下 硬绑定 物理上是如何工作的,也就是 Function.prototype.bind(..) 创建了一个新的包装函数,这个函数被硬编码为忽略它自己的 this 绑定(不管它是什么),转而手动使用我们提供的。

因此,这似乎看起来很明显,硬绑定(明确绑定的一种)的优先级要比 new 绑定 高,而且不能被 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

哇!bar 是硬绑定到 obj1 的,但是 new bar(3) 并 没有 像我们期待的那样将 obj1.a 变为 3。反而,硬绑定(到 obj1)的 bar(..) 调用 可以 被 new 所覆盖。因为 new 被覆盖,我们得到一个名为 baz 的新创建的对象,而且我们确实看到 baz.a 的值为 3

为什么 new 可以覆盖 硬绑定 这件事很有用?

这种行为的主要原因是,创建一个实质上忽略 this硬绑定 而预先设置一部分或所有的参数的函数(这个函数可以与 new 一起使用来构建对象)。bind(..) 的一个能力是,任何在第一个 this 绑定参数之后被传入的参数,默认地作为当前函数的标准参数(技术上这称为“局部应用(partial application)”,是一种“柯里化(currying)”)。

例如:

function foo(p1,p2) {
    this.val = p1 + p2;
}
// 在这里使用 `null` 是因为在这种场景下我们不关心 `this` 的硬绑定
// 而且反正它将会被 `new` 调用覆盖掉!
var bar = foo.bind( null, "p1" );
var baz = new bar( "p2" );
baz.val; // p1p2

判定 this

现在,我们可以按照优先顺序来总结一下从函数调用的调用点来判定 this 的规则了。按照这个顺序来问问题,然后在第一个规则适用的地方停下。

  1. 函数是通过 new 被调用的吗(new 绑定)?如果是,this 就是新构建的对象。

    var bar = new foo()

  2. 函数是通过 callapply 被调用(明确绑定),甚至是隐藏在 bind 硬绑定 之中吗?如果是,this 就是那个被明确指定的对象。

    var bar = foo.call( obj2 )

  3. 函数是通过环境对象(也称为拥有者或容器对象)被调用的吗(隐含绑定)?如果是,this 就是那个环境对象。

    var bar = obj1.foo()

  4. 否则,使用默认的 this默认绑定)。如果在 strict mode 下,就是 undefined,否则是 global 对象。

    var bar = foo()

以上,就是理解对于普通的函数调用来说的 this 绑定规则。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,997评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,603评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,359评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,309评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,346评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,258评论 1 300
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,122评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,970评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,403评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,596评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,769评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,464评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,075评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,705评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,848评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,831评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,678评论 2 354