this全面解析
# this全面解析
每个函数的 this 是在调用 时被绑定的,完全取决于函数的调用位置(也就是函数的调用方法)。
# 调用位置
调用位置就是函数在代码中被调用的 位置(而不是声明的位置)。
最重要的是要分析调用栈(就是为了到达当前执行位置所调用的所有函数)。
# 绑定规则
在函数的执行过程中调用位置如何决定 this 的绑定对象。
默认绑定
独立函数调用。
声明在全局作用域中的变量就是全局对 象的一个同名属性。它们本质上就是同一个东西,并不是通过复制得到的
this.a 被解析成了全局变量 a。为什么?
函数调用时应用了 this 的默认绑定,因此 this 指向全局对象。
隐式绑定
隐式丢失
一个最常见的 this 绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把 this 绑定到全局对象或者 undefined 上,取决于是否是严格模式。
丢失绑定问题。
参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值
function setTimeout(fn, delay) {
// 等待 delay 毫秒
fn(); // 调用位置
}
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 ); };
我们创建了函数 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);
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
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
2
3
4
5
这些函数实际上就是通过 call(..) 或者 apply(..) 实现了显式绑定
new绑定
something = new MyClass(..)
JavaScript 中 new 的机制实 际上和面向类的语言完全不同。
内置对象函数在内的所有函数都可 以用 new 来调用,这种函数调用被称为构造函数调用。
实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。
使用 new 来调用函数
会自动执行下面的操作
- 创建(或者说构造)一个全新的对象。
- 这个新对象会被执行 [[ 原型 ]] 连接。
- 这个新对象会绑定到函数调用的 this。
- 如果函数没有返回其他对象,那么 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;
}
}
}
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
2
3
4
5
6
7
8
9
10
11
# 判断this
- 函数是否在 new 中调用(new 绑定)?如果是的话 this 绑定的是新创建的对象。
var bar = new foo()
- 函数是否通过 call、apply(显式绑定)或者硬绑定调用?如果是的话,this 绑定的是 指定的对象。
var bar = foo.call(obj2)
- 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上 下文对象。
var bar = obj1.foo()
- 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 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;
};
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# this词法
箭头函数并不是使用 function 关键字定义的,而是使用被称为“胖箭头”的操作符 => 定 义的。箭头函数不使用 this 的四种标准规则,而是根据外层(函数或者全局)作用域来决 定 this。
- 只使用词法作用域并完全抛弃错误 this 风格的代码;
- 完全采用 this 风格,在必要时使用 bind(..),尽量避免使用 self = this 和箭头函数。
这两种代码风格的程序可以正常运行,但是在同一个函数或者同一个程序中混 合使用这两种风格通常会使代码更难维护,并且可能也会更难编写
# 小结
如果要判断一个运行中函数的 this 绑定,就需要找到这个函数的直接调用位置。找到之后 就可以顺序应用下面这四条规则来判断 this 的绑定对象。
- 由 new 调用?绑定到新创建的对象。
- 由 call 或者 apply(或者 bind)调用?绑定到指定的对象。
- 由上下文对象调用?绑定到那个上下文对象。
- 默认:在严格模式下绑定到 undefined,否则绑定到全局对象
一定要注意,有些调用可能在无意中使用默认绑定规则。如果想“更安全”地忽略 this 绑 定,你可以使用一个 DMZ 对象 Object.create(null),以保护全局对象。
ES6 中的箭头函数是根据当前的词法作用域来决定 this,具体来说,箭头函数会继承外层函数调用的 this 绑定