this

1.this的实质是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用

2.绑定规则

(1)默认绑定 : 对独立函数调用

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

var a = 2;
foo();//2

this解析为windows

function foo() {
"use strict";
console.log( this.a );
}
var a = 2;
foo(); // TypeError: this is undefined

虽然this的绑定规则完全取决调用位置,但仅仅只是在非strict mode下;

strict mode 下与其调用位置无关

function foo() {
console.log( this.a );
}
var a = 2;
(function(){
"use strict";
foo(); // 2
})();

(2)隐式绑定

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

首先需要注意的是 foo() 的声明方式,及其之后是如何被当作引用属性添加到 obj 中的。
但是无论是直接在 obj 中定义还是先定义再添加为引用属性,这个函数严格来说都不属于
obj 对象。
然而,调用位置会使用 obj 上下文来引用函数,因此你可以说函数被调用时 obj 对象“拥
有”或者“包含”它。
无论你如何称呼这个模式,当 foo() 被调用时,它的落脚点确实指向 obj 对象。当函数引
用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。因为调
用 foo() 时 this 被绑定到 obj,因此 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

(3)隐式丢失
一个最常见的 this 绑定问题就是被隐式绑定的函数会丢失绑定对象,

也就是说它会应用默认绑定,从而把 this 绑定到全局对象或者 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"

3.显式绑定

 就像我们刚才看到的那样,在分析隐式绑定时,我们必须在一个对象内部包含一个指向函

数的属性,并通过这个属性间接引用函数,从而把 this 间接(隐式)绑定到这个对象上。
那么如果我们不想在对象内部包含函数引用,而想在某个对象上强制调用函数,该怎么
做呢?
JavaScript 中的“所有”函数都有一些有用的特性(这和它们的 [[ 原型 ]] 有关——之后我
们会详细介绍原型),可以用来解决这个问题。具体点说,可以使用函数的 call(..) 和
apply(..) 方法。严格来说,JavaScript 的宿主环境有时会提供一些非常特殊的函数,它们
并没有这两个方法。但是这样的函数非常罕见,JavaScript 提供的绝大多数函数以及你自
己创建的所有函数都可以使用 call(..) 和 apply(..) 方法。
这两个方法是如何工作的呢?它们的第一个参数是一个对象,它们会把这个对象绑定到
this,接着在调用函数时指定这个 this。因为你可以直接指定 this 的绑定对象,因此我
们称之为显式绑定。

 4. new绑定

首先我们重新定义一下 JavaScript 中的“构造函数”。在 JavaScript 中,构造函数只是一些
使用 new 操作符时被调用的函数。它们并不会属于某个类,也不会实例化一个类。实际上,
它们甚至都不能说是一种特殊的函数类型,它们只是被 new 操作符调用的普通函数而已。
举例来说,思考一下 Number(..) 作为构造函数时的行为,ES5.1 中这样描述它:
15.7.2 Number 构造函数
当 Number 在 new 表达式中被调用时,它是一个构造函数:它会初始化新创建的
对象。
所以,包括内置对象函数(比如 Number(..),详情请查看第 3 章)在内的所有函数都可
以用 new 来调用,这种函数调用被称为构造函数调用。这里有一个重要但是非常细微的区
别:实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。
使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。
1. 创建(或者说构造)一个全新的对象。
2. 这个新对象会被执行 [[ 原型 ]] 连接。
3. 这个新对象会绑定到函数调用的 this。
4. 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。

function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log( bar.a ); // 2

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

5.优先级

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 中使用硬绑定函数,主要目的是预先设置函数的一些参数,这样在使用
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

 

6.判断this
现在我们可以根据优先级来判断函数在某个调用位置应用的是哪条规则。可以按照下面的
顺序来进行判断:
(1) 函数是否在 new 中调用(new 绑定)?如果是的话 this 绑定的是新创建的对象。
var bar = new foo()
(2) 函数是否通过 call、apply(显式绑定)或者硬绑定调用?如果是的话,this 绑定的是
指定的对象。
var bar = foo.call(obj2)
(3) 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上
下文对象。
var bar = obj1.foo()
(4)如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined,否则绑定到
全局对象。
var bar = foo()
就是这样。对于正常的函数调用来说,理解了这些知识你就可以明白 this 的绑定原理了。
不过……凡事总有例外。

如果你把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind,这些值
在调用时会被忽略,实际应用的是默认绑定规则:
function foo() {
console.log( this.a );
}
var a = 2;
foo.call( null ); // 2
那么什么情况下你会传入 null 呢?
一种非常常见的做法是使用 apply(..) 来“展开”一个数组,并当作参数传入一个函数。
类似地,bind(..) 可以对参数进行柯里化(预先设置一些参数),这种方法有时非常有用:
function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// 把数组“展开”成参数
foo.apply( null, [2, 3] ); // a:2, b:3
// 使用 bind(..) 进行柯里化
var bar = foo.bind( null, 2 );
bar( 3 ); // a:2, b:3
这两种方法都需要传入一个参数当作 this 的绑定对象。如果函数并不关心 this 的话,你
仍然需要传入一个占位值,这时 null 可能是一个不错的选择,就像代码所示的那样。
但在 ES6 中,可以用 ... 操作符代替 apply(..) 来“展
开”数组,foo(...[1,2]) 和 foo(1,2) 是一样的,这样可以避免不必要的
this 绑定。可惜,在 ES6 中没有柯里化的相关语法,因此还是需要使用
bind(..)。
然而,总是使用 null 来忽略 this 绑定可能产生一些副作用。如果某个函数确实使用了
this(比如第三方库中的一个函数),那默认绑定规则会把 this 绑定到全局对象(在浏览
器中这个对象是 window),这将导致不可预计的后果(比如修改全局对象)。
显而易见,这种方式可能会导致许多难以分析和追踪的 bug。

(5)更安全的this
一种“更安全”的做法是传入一个特殊的对象,把 this 绑定到这个对象不会对你的程序
产生任何副作用。就像网络(以及军队)一样,我们可以创建一个“DMZ”(demilitarized
zone,非军事区)对象——它就是一个空的非委托的对象(委托在第 5 章和第 6 章介绍)。
如果我们在忽略 this 绑定时总是传入一个 DMZ 对象,那就什么都不用担心了,因为任何
对于 this 的使用都会被限制在这个空对象中,不会对全局对象产生任何影响。
由于这个对象完全是一个空对象,我自己喜欢用变量名 ø(这是数学中表示空集合符号的
小写形式)来表示它。在大多数键盘(比如说 Mac 的 US 布局键盘)上都可以使用⌥ +o
(Option-o)来打出这个符号。有些系统允许你为特殊符号设定快捷键。如果你不喜欢 ø 符
号或者你的键盘不太容易打出这个符号,那你可以换一个喜欢的名字来称呼它。
无论你叫它什么,在 JavaScript 中创建一个空对象最简单的方法都是 Object.create(null)
( 详 细 介 绍 请 看 第 5 章 )。Object.create(null) 和 {} 很 像, 但 是 并 不 会 创 建 Object.
prototype 这个委托,所以它比 {}“更空”:
function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// 我们的 DMZ 空对象
var ø = Object.create( null );
// 把数组展开成参数
foo.apply( ø, [2, 3] ); // a:2, b:3
// 使用 bind(..) 进行柯里化
var bar = foo.bind( ø, 2 );
bar( 3 ); // a:2, b:3
使用变量名 ø 不仅让函数变得更加“安全”,而且可以提高代码的可读性,因为 ø 表示
“我希望 this 是空”,这比 null 的含义更清楚。不过再说一遍,你可以用任何喜欢的名字
来命名 DMZ 对象。
 间接引用
另一个需要注意的是,你有可能(有意或者无意地)创建一个函数的“间接引用”,在这
种情况下,调用这个函数会应用默认绑定规则。
间接引用最容易在赋值时发生:
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()。根据我们之前说过的,这里会应用默认绑定。
注意:对于默认绑定来说,决定 this 绑定对象的并不是调用位置是否处于严格模式,而是
函数体是否处于严格模式。如果函数体处于严格模式,this 会被绑定到 undefined,否则
this 会被绑定到全局对象。
(6)软绑定
之前我们已经看到过,硬绑定这种方式可以把 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;
};
}
除了软绑定之外,softBind(..) 的其他原理和 ES5 内置的 bind(..) 类似。它会对指定的函
数进行封装,首先检查调用时的 this,如果 this 绑定到全局对象或者 undefined,那就把
指定的默认对象 obj 绑定到 this,否则不会修改 this。此外,这段代码还支持可选的柯里
化(详情请查看之前和 bind(..) 相关的介绍)。

下面我们看看 softBind 是否实现了软绑定功能:
function foo() {
console.log("name: " + this.name);
}
var obj = { name: "obj" },
obj2 = { name: "obj2" },
obj3 = { name: "obj3" };
var fooOBJ = foo.softBind( obj );
fooOBJ(); // name: obj
obj2.foo = foo.softBind(obj);
obj2.foo(); // name: obj2 <---- 看!!!
fooOBJ.call( obj3 ); // name: obj3 <---- 看!
setTimeout( obj2.foo, 10 );
// name: obj <---- 应用了软绑定
可以看到,软绑定版本的 foo() 可以手动将 this 绑定到 obj2 或者 obj3 上,但如果应用默
认绑定,则会将 this 绑定到 obj。

(7)this 的词法

箭头函数并不是使用 function 关键字定义的,而是使用被称为“胖箭头”的操作符 => 定
义的。箭头函数不使用 this 的四种标准规则,而是根据外层(函数或者全局)作用域来决
定 this。
我们来看看箭头函数的词法作用域:
function foo() {
// 返回一个箭头函数
return (a) => {
//this 继承自 foo()
console.log( this.a );
};
}
var obj1 = {
a:2
};
var obj2 = {
a:3
};
var bar = foo.call( obj1 );
bar.call( obj2 ); // 2, 不是 3 !
foo() 内部创建的箭头函数会捕获调用时 foo() 的 this。由于 foo() 的 this 绑定到 obj1,
bar(引用箭头函数)的 this 也会绑定到 obj1,箭头函数的绑定无法被修改。(new 也不
行!)
箭头函数最常用于回调函数中,例如事件处理器或者定时器:
function foo() {
setTimeout(() => {
// 这里的 this 在此法上继承自 foo()
console.log( this.a );
},100);
}
var obj = {
a:2
};
foo.call( obj ); // 2
箭头函数可以像 bind(..) 一样确保函数的 this 被绑定到指定对象,此外,其重要性还体
现在它用更常见的词法作用域取代了传统的 this 机制。实际上,在 ES6 之前我们就已经
在使用一种几乎和箭头函数完全一样的模式。
function foo() {
var self = this; // lexical capture of this
setTimeout( function(){
console.log( self.a );
}, 100 );
}
var obj = {
a: 2
};
foo.call( obj ); // 2
虽然 self = this 和箭头函数看起来都可以取代 bind(..),但是从本质上来说,它们想替
代的是 this 机制。
如果你经常编写 this 风格的代码,但是绝大部分时候都会使用 self = this 或者箭头函数
来否定 this 机制,那你或许应当:
(1) 只使用词法作用域并完全抛弃错误 this 风格的代码;
(2)完全采用 this 风格,在必要时使用 bind(..),尽量避免使用 self = this 和箭头函数。

posted @ 2023-06-13 16:48  青Fire  阅读(70)  评论(0编辑  收藏  举报