this全面解析

# this全面解析

每个函数的 this 是在调用 时被绑定的,完全取决于函数的调用位置(也就是函数的调用方法)。

# 调用位置

调用位置就是函数在代码中被调用的 位置(而不是声明的位置)。

最重要的是要分析调用栈(就是为了到达当前执行位置所调用的所有函数)。

# 绑定规则

在函数的执行过程中调用位置如何决定 this 的绑定对象。

默认绑定

独立函数调用。

声明在全局作用域中的变量就是全局对 象的一个同名属性。它们本质上就是同一个东西,并不是通过复制得到的

this.a 被解析成了全局变量 a。为什么?

函数调用时应用了 this 的默认绑定,因此 this 指向全局对象。

隐式绑定

隐式丢失

一个最常见的 this 绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把 this 绑定到全局对象或者 undefined 上,取决于是否是严格模式。

丢失绑定问题。

参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值

function setTimeout(fn, delay) {
 // 等待 delay 毫秒
 fn(); // 调用位置
}
1
2
3
4

在分析隐式绑定时,我们必须在一个对象内部包含一个指向函 数的属性,并通过这个属性间接引用函数,从而把 this 间接(隐式)绑定到这个对象上。

丢失绑定问题。

显式绑定

可以使用函数的 call(..) 和 apply(..) 方法。

它们的第一个参数是一个对象,它们会把这个对象绑定到 this,接着在调用函数时指定这个 this。因为你可以直接指定 this 的绑定对象,因此我 们称之为显式绑定。

如果你传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作 this 的绑定对 象,这个原始值会被转换成它的对象形式(也就是 new String(..)、new Boolean(..) 或者 new Number(..))。这通常被称为“装箱”。

从 this 绑定的角度来说,call(..) 和 apply(..) 是一样的,它们的区别体现 在其他的参数上

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

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

硬绑定的典型应用场景就是创建一个包裹函数,传入所有的参数并返回接收到的所有值:

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);
console.log(b);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

另一种使用的方法是创建一个i可以重复使用的辅助函数:

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

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

var obj = {
 a: 2
}

var bar = bind(foo, obj);

var b = bar(3);
console.log(b); // 5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

ES5 中提供了内置的方法 Function.prototype. bind

bind(..) 会返回一个硬编码的新函数,它会把参数设置为 this 的上下文并调用原始函数

API调用的“上下文"

function foo(el) { console.log( el, this.id ); }
var obj = { id: "awesome" };// 调用 foo(..) 时把 this 绑定到 

obj [1, 2, 3].forEach( foo, obj ); 
// 1 awesome 2 awesome 3 awesome
1
2
3
4
5

这些函数实际上就是通过 call(..) 或者 apply(..) 实现了显式绑定

new绑定

something = new MyClass(..)

JavaScript 中 new 的机制实 际上和面向类的语言完全不同。

内置对象函数在内的所有函数都可 以用 new 来调用,这种函数调用被称为构造函数调用。

实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。

使用 new 来调用函数

会自动执行下面的操作

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

使用 new 来调用 foo(..) 时,我们会构造一个新对象并把它绑定到 foo(..) 调用中的 this 上。new 是最后一种可以影响函数调用时 this 绑定行为的方法,我们称之为 new 绑定。

# 优先级

隐式绑定和显式绑定哪个优先级更高? 显式绑定优先级更高

new 绑定和隐式绑定的优先级谁高谁低

new 绑定比隐式绑定优先级高

new 绑定和显式绑定谁的优先级更高呢?

if (!Function.prototype.bind) {
 Function.prototype.bind = function(oThis) {
  if (typeof this !== 'function') {
   // 与 ES5 最接近的
   // 内部 IsCallable 函数
   throw new TypeError(
    'Function.prototype.bind - what is trying ' +
    'to be bound is not callabe'
   );
  }
  var aArgs = Array.prototype.slice.call(arguments, 1),
  fToBind = this,
  fNOP = function() {},
  fBound = function() {
   return fToBind.apply(
    (
     this instanceof fNOP && oThis ? this : oThis
    ),
    aArgs.concat(
     Array.prototype.slice.call(arguments)
    );
   );
   fNOP.prototype = this.prototype;
   fBound.prototype = new fNOP();
   return fBound;
  }
 }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

为什么要在 new 中使用硬绑定函数呢?

在 new 中使用硬绑定函数,主要目的是预先设置函数的一些参数,这样在使用 new 进行初始化时就可以只传入其余的参数。bind(..) 的功能之一就是可以把除了第一个 参数(第一个参数用于绑定 this)之外的其他参数都传给下层的函数(这种技术称为“部 分应用”,是“柯里化”的一种)

function foo(p1, p2) {
 this.val = p1 + p2;
}

// 之所以使用null是因为我们并不关心硬绑定的this是什么
// 反正使用new时 this 会被修改
var bar = foo.bind(null, 'p1');

var baz = new bar('p2');

baz.val; // p1p2
1
2
3
4
5
6
7
8
9
10
11

# 判断this

  1. 函数是否在 new 中调用(new 绑定)?如果是的话 this 绑定的是新创建的对象。

var bar = new foo()

  1. 函数是否通过 call、apply(显式绑定)或者硬绑定调用?如果是的话,this 绑定的是 指定的对象。

var bar = foo.call(obj2)

  1. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上 下文对象。

var bar = obj1.foo()

  1. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined,否则绑定到 全局对象。

var bar = foo()

# 绑定例外

被忽略的this

如果你把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind,这些值 在调用时会被忽略,实际应用的是默认绑定规则

那么什么情况下你会传入 null 呢?

一种非常常见的做法是使用 apply(..) 来“展开”一个数组,并当作参数传入一个函数。

更安全的this

一种“更安全”的做法是传入一个特殊的对象,把 this 绑定到这个对象不会对你的程序 产生任何副作用。

无论你叫它什么,在 JavaScript 中创建一个空对象最简单的方法都是 Object.create(null)

Object.create(null) 和 {} 很 像, 但 是 并 不 会 创 建 Object. prototype 这个委托,所以它比 {}“更空”

间接引用

间接引用最容易在赋值时发生:

(p.foo = o.foo)(); // 2

赋值表达式 p.foo = o.foo 的返回值是目标函数的引用,因此调用位置是 foo() 而不是 p.foo() 或者 o.foo()。根据我们之前说过的,这里会应用默认绑定。

注意:对于默认绑定来说,决定 this 绑定对象的并不是调用位置是否处于严格模式,而是 函数体是否处于严格模式。如果函数体处于严格模式,this 会被绑定到 undefined,否则 this 会被绑定到全局对象。

软绑定

硬绑定这种方式可以把 this 强制绑定到指定的对象(除了使用 new 时),防止函数调用应用默认绑定规则。

问题在于,硬绑定会大大降低函数的灵活性,使 用硬绑定之后就无法使用隐式绑定或者显式绑定来修改 this。

如果可以给默认绑定指定一个全局对象和 undefined 以外的值,那就可以实现和硬绑定相 同的效果,同时保留隐式绑定或者显式绑定修改 this 的能力。

if (!Function.prototype.softBind) {
 Function.prototype.softBind = function(obj) {
  var fn = this;
  // 捕获所有 curried 参数
  var curried = [].slice.call(arguments, 1);
  var bound = function() {
   return fn.apply(
    (!this || this === (window || global)) ?
     obj : this
     curried.concat.apply(curried, arguments)
   );
  };
  bound.prototype = Object.create(fn.prototype);
  return bound;
 };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# this词法

箭头函数并不是使用 function 关键字定义的,而是使用被称为“胖箭头”的操作符 => 定 义的。箭头函数不使用 this 的四种标准规则,而是根据外层(函数或者全局)作用域来决 定 this。

  1. 只使用词法作用域并完全抛弃错误 this 风格的代码;
  2. 完全采用 this 风格,在必要时使用 bind(..),尽量避免使用 self = this 和箭头函数。

这两种代码风格的程序可以正常运行,但是在同一个函数或者同一个程序中混 合使用这两种风格通常会使代码更难维护,并且可能也会更难编写

# 小结

如果要判断一个运行中函数的 this 绑定,就需要找到这个函数的直接调用位置。找到之后 就可以顺序应用下面这四条规则来判断 this 的绑定对象。

  1. 由 new 调用?绑定到新创建的对象。
  2. 由 call 或者 apply(或者 bind)调用?绑定到指定的对象。
  3. 由上下文对象调用?绑定到那个上下文对象。
  4. 默认:在严格模式下绑定到 undefined,否则绑定到全局对象

一定要注意,有些调用可能在无意中使用默认绑定规则。如果想“更安全”地忽略 this 绑 定,你可以使用一个 DMZ 对象 Object.create(null),以保护全局对象。

ES6 中的箭头函数是根据当前的词法作用域来决定 this,具体来说,箭头函数会继承外层函数调用的 this 绑定

上次更新: 2022/7/25 下午12:50:39