你不懂js系列学习笔记-this与对象原型- 02

本文深入讲解JavaScript中的this绑定规则,包括默认绑定、隐含绑定、明确绑定和new绑定,并探讨了这些规则之间的优先级。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

第二章: this 豁然开朗!

原文:You-Dont-Know-JS

在第一章中,我们摒弃了种种对 this 的误解,并且知道了 this 是一个完全根据调用点(函数是如何被调用的)而为每次函数调用建立的绑定。

1 调用点(Call-site)

为了理解 this 绑定,我们不得不理解调用点:函数在代码中被调用的位置(不是被声明的位置)。我们必须考察调用点来回答这个问题:这个 this 指向什么?

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

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

我们来展示一下调用栈和调用点:

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

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

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

  console.log("bar");
  foo(); // <-- `foo` 的 call-site
}

function foo() {
  // 调用栈是: `baz` -> `bar` -> `foo`
  // 我们的调用点位于 `bar`

  console.log("foo");
}

baz(); // <-- `baz` 的调用点
复制代码

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

注意: 你可以通过按顺序观察函数的调用链在你的大脑中建立调用栈的视图,就像我们在上面代码段中的注释那样。但是这很痛苦而且易错。另一种观察调用栈的方式是使用你的浏览器的调试工具。大多数现代的桌面浏览器都内建开发者工具,其中就包含 JS 调试器。在上面的代码段中,你可以在调试工具中为 foo() 函数的第一行设置一个断点,或者简单的在这第一行上插入一个 debugger 语句。当你运行这个网页时,调试工具将会停止在这个位置,并且向你展示一个到达这一行之前所有被调用过的函数的列表,这就是你的调用栈。所以,如果你想调查this 绑定,可以使用开发者工具取得调用栈,之后从上向下找到第二个记录,那就是你真正的调用点。

2 this 指向规则

你必须考察调用点并判定 4 种规则中的哪一种适用。我们将首先独立地解释一下这 4 种规则中的每一种,之后我们来展示一下如果有多种规则可以适用于调用点时,它们的优先顺序。

与这四种绑定规则不同,ES6 的箭头方法使用词法作用域来决定 this 绑定,这意味着它们采用封闭他们的函数调用作为 this 绑定(无论它是什么)。它们实质上是 ES6 之前的 self = this 代码的语法替代品。

2.1 默认绑定(Default Binding)

我们要考察的第一种规则源于函数调用的最常见的情况:独立函数调用。可以认为这种 this 规则是在没有其他规则适用时的默认规则。

考虑这个代码段:

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

var a = 2;

foo(); // 2
复制代码

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

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

function foo() {
  "use strict";

  console.log(this.a);
}

var a = 2;

foo(); // TypeError: `this` is `undefined`
复制代码

一个微妙但是重要的细节是:即便所有的 this 绑定规则都是完全基于调用点的,但如果 foo()内容 没有在 strict mode下执行,对于 默认绑定 来说全局对象是 唯一 合法的;foo() 的调用点的 strict mode 状态与此无关。

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

var a = 2;

(function() {
  "use strict";

  foo(); // 2
})();
复制代码

2.2 隐含绑定(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 似乎是 obj.foo 的引用,但实际上它只是另一个 foo 本身的引用而已。另外,起作用的调用点是 bar(),一个直白,毫无修饰的调用,因此 默认绑定 适用于这里。

这种情况发生的更加微妙,更常见,而且更意外的方式,是当我们考虑传递一个回调函数时:

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

function doFoo(fn) {
  // `fn` 只不过 `foo` 的另一个引用

  fn(); // <-- 调用点!
}

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

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

doFoo(obj.foo); // "oops, global"
复制代码

那么如果接收你所传递回调的函数不是你的,而是语言内建的呢?没有区别,同样的结果。

另外一个要注意的是,你可以(有意或无意地!)创建对函数的“间接引用(indirect reference)”,在那样的情况下,当那个函数引用被调用时,默认绑定 规则也会适用。

一个最常见的 间接引用 产生方式是通过赋值:

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

var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };

o.foo(); // 3
(p.foo = o.foo)(); // 2
复制代码

赋值表达式 p.foo = o.foo结果值 是一个刚好指向底层函数对象的引用。如此,起作用的调用点就是 foo(),而非你期待的 p.foo()o.foo()。根据上面的规则,默认绑定适用。

2.3 明确绑定(Explicit Binding)

函数拥有 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

如果你传递一个简单基本类型值(stringboolean,或 number 类型)作为 this 绑定,那么这个基本类型值会被包装在它的对象类型中(分别是 new String(..)new Boolean(..),或 new Number(..))。这通常称为“封箱(boxing)”。

硬绑定(Hard Binding)

解决 this 指向隐含丢失的问题, 利用明确绑定的变种确实可以实现这个技巧。考虑这段代码:

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

var obj = {
  a: 2
};

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

bar(); // 2
setTimeout(bar, 100); // 2

// `bar` 将 `foo` 的 `this` 硬绑定到 `obj`
// 所以它不可以被覆盖
bar.call(window); // 2
复制代码

硬绑定 将一个函数包装起来的最典型的方法,是为所有传入的参数和传出的返回值创建一个通道:

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

var obj = {
  a: 2
};

var bar = function() {
  return foo.apply(obj, arguments);
};

var b = bar(3); // 2 3
console.log(b); // 5
复制代码

另一种表达这种模式的方法是创建一个可复用的帮助函数:

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

// 简单的 `bind` 帮助函数
function bind(fn, obj) {
  return function() {
    return fn.apply(obj, arguments);
  };
}

var obj = {
  a: 2
};

var bar = bind(foo, obj);

var b = bar(3); // 2 3
console.log(b); // 5
复制代码

由于 硬绑定 是一个如此常用的模式,它已作为 ES5 的内建工具提供:Function.prototype.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
复制代码

bind(..) 返回一个硬编码的新函数,它使用你指定的 this 环境来调用原本的函数。

2.4 new 绑定(new Binding)

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

可以说任何函数,包括像 Number(..)(见第三章)这样的内建对象函数都可以在前面加上 new 来被调用,这使函数调用成为一个 构造器调用(constructor call)。这是一个重要而微妙的区别:实际上不存在“构造器函数”这样的东西,而只有函数的构造器调用。

当在函数前面被加入 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)。

3 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
复制代码

注意: newcall/apply 不能同时使用,所以 new foo.call(obj1) 是不允许的,也就是不能直接对比测试 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
复制代码

如果你传递 nullundefined 作为 callapplybindthis 绑定参数,那么这些值会被忽略掉,取而代之的是 默认绑定 规则将适用于这个调用。

所以我们可以按照优先顺序来总结一下从函数调用的调用点来判定 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()

转载于:https://juejin.im/post/5adf23d951882567286002de

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值