1 关于this
this关键字是JavaScript中最复杂的机制之一。它是一个很特别的关键字,被自动定义在所有函数的作用域中。
1.1 为什么要用this
function identify(){
return this.name.toUpperCase();
}
function speak(){
var greeting = `Hello, I'm ${identify.call(this)}`;
return greeting;
}
var me = {
name: 'Kyle'
}
var you ={
name: 'Reader'
}
console.log(identify.call(me)); //KYLE
console.log(identify.call(you)); //READER
console.log(speak.call(me)); //Hello, I'm KYLE
console.log(speak.call(you)); //Hello, I'm READER
上面的代码可以在不同的上下文对象(me 和 you)中重复使用函数 identify() 和 speak(),不用针对每个对象编写不同版本的函数。如果不使用 this,则需要给 identify() 和 speak() 传入一个显式的上下文对象:
function identify(context){
return context.name.toUpperCase();
}
function speak(context){
var greeting = `Hello, I'm ${identify(context)}`;
return greeting;
}
var me = {
name: 'Kyle'
}
var you ={
name: 'Reader'
}
console.log(identify(me)); //KYLE
console.log(identify(you)); //READER
console.log(speak(me)); //Hello, I'm KYLE
console.log(speak(you)); //Hello, I'm READER
随着使用模式越来越复杂,显式传递上下文对象会让代码变得越来越混乱,使用this则不会这样。this提供了一种跟优雅的方式隐式“传递”一个对象引用,可以将API设计得更加简洁并且易于复用。
1.2 关于this的错误认识
(1)将this理解成指向函数自身
function foo(num){
console.log(`foo: ${num}`);
this.count++;
}
foo.count = 0;
for (var i = 0; i<10; i++){
if(i>5){
foo(i);
}
}
console.log(foo.count);
// foo: 6
// this.count: NaN
// foo: 7
// this.count: NaN
// foo: 8
// this.count: NaN
// foo: 9
// this.count: NaN
// 0
执行上述代码,会发现congsole.log()语句产生了4条输出,证明foo(..)确实被调用了4次,但foo.count的值仍然是0,显然foo()中的this并没有指向foo()自身,因此不能单纯从字面意思来理解this。事实上这段代码真正创建的是一个全局变量count,即this指向了全局对象,这个可在后续笔记中发现原理。
(2)将this理解成函数的作用域
将this理解成指向函数的作用域在某种情况下是正确的,但在其他情况下却是错误的,但需要明确的是,在任何情况情况下,this都不指向函数的词法作用域。在JavaScript内部,作用域确实和对象类似,可见的标识符都是它的属性,但是作用域"对象"无法通过JavaScript代码访问,它存在于JavaScript引擎内部。下面的代码试图使用this来隐式引用函数的词法作用域,但并没有成功:
function foo(){
var a = 2;
this.bar(); //此处调用成功纯属意外,最自然调用bar()的方法是省略前面的this,直接使用词法引用标识符
}
function bar(){
console.log( this.a ); //此处试图使用this联通foo()和bar()的词法作用域,从而让bar()可以访问foo()作用域里的变量a,但这是不可能实现的,使用this不可能在词法作用域中查到什么
}
foo(); //undefined
每当想要把this和词法作用域的查找混合使用时,一定要谨记这是无法实现的。
1.3 this到底是什么
this实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。
this是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件,this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
当一个函数被调用时,会创建一个活动记录(有时也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息。this就是这个记录的一个属性,会在函数执行的过程中用到。
2. this全面解析
2.1 调用位置
在理解this的绑定过程之前,首先要理解调用位置:调用位置就是函数在代码中被调用的位置(而不是声明的位置),只用仔细分析调用位置才能明确:这个this到底引用的是什么。
通常来说,寻找调用位置就是寻找“函数被调用的位置”,只是某些编程模式可能会隐藏真正的调用位置。最重要的是分析调用栈(就是为了到达当前执行位置所调用的所有函数),所关心的调用位置就在当前正在执行的函数的前一个调用中。
调用栈和调用位置:
function baz(){
//当前 调用栈:baz
//因此当前 调用位置 是:全局作用域
console.log("baz");
bar();
}
function bar(){
//当前 调用栈:baz -> bar
//因此当前 调用位置 在 baz 中
console.log("bar");
foo();
}
function foo(){
//当前 调用栈:baz -> bar -> foo
//因此当前 调用位置 在 foo 中
console.log("foo");
}
baz(); // <-- baz() 的调用位置
//baz
//bar
//foo
需注意是如何(从调用栈中)分析出真正的调用位置的,因为它决定了this的绑定。
2.2 绑定规则
以下显示的打印结果均为浏览器引擎环境下的结果,具有全局对象window。
2.2.1 默认绑定 - 独立函数调用时
独立函数调用是最常用的函数调用类型。默认绑定可以看作是无法应用其他规则时的默认规则。例:
/** 非严格模式 */
function foo() {
console.log(this.a); //浏览器引擎环境下打印出 2,node环境下打印出 undefined
}
var a = 2;
foo();
/**
* foo():浏览器引擎环境下打印出 2,node环境下打印出 undefined,因为此处this为默认绑定全局对象,浏览器引擎环境下即为window。
* 代码中,foo()是直接使用不带任何修饰的函数引用进行调用的,因此this只能使用默认绑定,无法应用其他规则。
* 声明在全局作用域中的变量(比如 var a = 2)就是全局对象的一个同名属性。它们本质上就是同一个东西,并不是通过复制得到的,就像一个硬币的两面一样。
*/
/** 严格模式 */
function foo() {
"use strict"; //foo()运行在strict mode下
console.log(this); //undefined
console.log(this.a); //Uncaught TypeError: Cannot read property 'a' of undefined
}
var a = 2;
foo();
/**若使用严格模式(strict mode),则全局对象将无法使用默认绑定,因此this会绑定到undefined。
* 虽然this的绑定规则完全取决于调用的位置,但是只有foo()运行在非strict mode下时,默认绑定才能绑定到全局对象;严格模式下调用foo()则不影响默认绑定
*/
function foo() {
console.log(this.a); //foo()运行在非strict mode下
}
var a = 2;
(function () {
"use strict"; //在严格模式下调用foo()不影响默认绑定
foo(); //浏览器引擎环境下打印出 2,node环境下打印出 undefined,
})();
注意:对于默认绑定来说,决定this绑定对象的并不是调用位置是否处于严格模式,而是函数体是否处于严格模式。如果函数体处于严格模式,this会被绑定到undefined,否则this会被绑定到全局对象。
2.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( `a: ${this.a}`);
}
var obj2 = {
a: 42,
foo: foo
}
var obj1 = {
a: 2,
obj2: obj2
}
obj1.obj2.foo(); // a: 42 ,即最后一层影响调用位置
一个最常见的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"
/**虽然 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"
setTimeout(obj.foo, 100); //"oops, global"
/**无论是把函数传入自定义的函数还是语言内置的函数,结果都是一样的,本质上应用了默认绑定 */
如上所述,回调函数丢失this绑定是非常常见的,除此之外,还有一种情况:调用回调函数的函数可能会修改this,例如在一些流行的JavaScript库中时间处理器常会把回调函数的this强制绑定到触发事件的DOM元素上。无论哪种情况,this的改变都是意想不到的,实际上无法控制回调函数的执行方式,也就无法控制会影响绑定的调用位置。可以通过固定this来修复这个问题。
2.2.3 显示绑定 - 不在对象内部包含函数引用,在某个对象上强制调用函数
隐式绑定时,必须在一个对象内部包含一个指向函数的属性,并通过这个属性简介引用函数,从而把this间接(隐式)绑定到这个对象上。若不想在对象内部包含函数引用,而想在某个对象上强制调用函数,则可利用 JavaScript 中的‘所有’函数都有的一些有用属性(与其原型有关)来解决这个问题:可以使用函数的call(...)和apply(...)方法。严格来说,JavaScript的宿主环境有时会提供一些非常特殊的函数,它们并没有这个两个方法,但是这样的函数非常罕见,JavaScript提供的绝大多数函数以及自定义的函数都可以使用call(...)和apply(...)方法。
call(...)和apply(...)方法工作原理:它们的第一个参数是一个对象,它们会把这个对象绑定到this,接着在调用函数时制定这个this。从this绑定的角度来说,call(..)和apply(..)是一样的,它们的区别体现在其他的参数上。
因为这种方式可以直接制定this的绑定对象,因此称之为显示绑定。例:
function foo(){
console.log(this.a);
}
var obj = {
a: 2
};
foo.call(obj); //2
/**通过foo.call(...),可以在调用foo时强制把它的this绑定到obj上。 */
若传入一个原始值(字符串类型、布尔类型或者数字类型)当作this的绑定对象,则这个原始值或被转换成它的对象形式(即:new String(..)、new Boolean(..)或者new Number(..)) 。这通常被成为“装箱”。
显示绑定仍然无法解决之前提出的绑定丢失的问题,但显示绑定的一个变种可以解决这个问题,称之为 硬绑定。例:
/** 硬绑定 */
function foo(){
console.log(this.a);
}
var obj = {
a: 2
}
var bar = function(){
foo.call(obj);
}
bar(); //2
setTimeout(bar, 100); // 2
// 硬绑定的bar不可能在修改它的this
bar.call(window); //2
/**分析这个显示绑定变种:创建了函数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); //2 3
console.log(b); //5
另一种使用方法是创建一个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); //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的上下文并调用原始函数。 */
API调用的“上下文” : 第三方库的许多函数,以及JavaScript语言和宿主环境中许多新的内置函数,都提供了一个可选的参数,通常被称为“上下文”(context),其作用和bind(..)一样,确保你的回调函数使用指定的this。例:
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
/**这些函数实际上就是通过call(..)或者apply(..)实现了显示绑定,这样可以少一些代码 */
2.2.4 new绑定
在传统的面向类的语言中,“构造函数”是类中的一种特殊方法,使用new初始化类时会调用类中的构造函数,通常形式是:someting = new MyClass(..)。JavaScript也有一个new操作符,使用方法看起来也和那些面向类的语言一样,绝大多数开发者都认为JavaScript中new的机制也和那些语言一样。然而,JavaScript中new的机制实际上和面向类的语言完全不同。
在JavaScript中,构造函数只是一些使用new操作符时被调用的函数。它们并不会属于某个类,也不会实例化一个类。实际上,它们甚至都不能说是一种特殊的函数类型,它们只是被new操作符调用的普通函数而已。举例来说,ES5.1中这样描述 Number(..) 作为构造函数时的行为:
15.7.2 Number构造函数
当 Number 在 new 表达式中被调用时,它是一个构造函数:它会初始化新创建的对象。
所以,包括内置对象函数(比如Number(..))在内的所有函数都可以用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上 */
2.3 四条this绑定规则的优先级
上述已经了解了函数调用中 this 绑定的四条规则,需要做的就是找到函数的调用位 置并判断应当应用哪条规则。若某个调用位置可以应用多条规则,则需要给这些规则设定优先级。
2.3.1 隐式绑定与显示绑定优先级
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 显示绑定
/** 可明显看出:显示绑定优先级 高于 隐式绑定优先级,即在判断时应当先考虑是否可以应用显示绑定 */
2.3.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(obj1.a); //2 隐式绑定
console.log(obj2.a); //3 显示绑定
var bar = new obj1.foo(4);
console.log(obj1.a); //2 隐式绑定
console.log(bar.a); //4 new绑定
/**可以看出:new绑定优先级 高于 隐式绑定优先级 */
2.3.3 显示绑定与new绑定优先级
new 和 call/apply 无法一起使用,因此无法通过 new foo.call(obj1)来直接进行测试。但是可以使用硬绑定来测试它俩的优先级。首先回忆硬绑定的工作过程: Function.prototype.bind(..)会创建一个新的包装函数,这个函数会忽略它当前的this绑定(无论绑定的对象是什么),并把我们提供的对象绑定到this上。这样看起来硬绑定(也是显示绑定的一种)似乎比new绑定的优先级更高,无法使用new来控制this绑定。下面通过实例验证一下:
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。相反,new修改了硬绑定(到obj1的)调用bar(..)中的this。因为使用了new绑定,得到了一个名字为baz的新对象,并且baz.a的值为3。
看看之前介绍的“裸”辅助函数bind:
function bind(fn, obj) {
return function () {
fn.apply(obj, arguments);
}
}
看起来在辅助函数中new操作符的调用无法修改this绑定,但是在上面的例子中new确实修改了this绑定。
实际上,ES5中内置的Function.prototype.bind(..)更加复杂。MDN提供了一种bind(..)实现,排版后如下:
if (!Function.prototype.bind) { Function.prototype.bind = function (oThis) { if (typeof this !== "function") { // 与 ECMAScript 5 最接近的 // 内部 IsCallable 函数 throw new TypeError( "Function.prototype.bind - what is trying " + "to be bound is not callable"); } 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; }; }
这种 bind(..) 是一种 polyfill 代码(polyfill 就是我们常说的刮墙用的腻子,polyfill 代码主要用于旧浏览器的兼容,比如说在旧的浏览器中并没有内置 bind 函数,因此可以使用 polyfill 代码在旧浏览器中实现新的功能),对于 new 使用的硬绑定函数来说,这段 polyfill 代码和 ES5 内置的 bind(..) 函数并不完全相同(后面会介绍为什么要在 new 中使用硬绑定函 数)。由于 polyfill 并不是内置函数,所以无法创建一个不包含 .prototype 的函数,因此会具有一些副作用。如果你要在 new 中使用硬绑定函数并且依 赖 polyfill 代码的话,一定要非常小心。
下面是new修改this的相关代码:
this instanceof fNOP && oThis ? this: oThis; //...以及: fNOP.prototype = this.prototype; fBound.protptype = new fNOP();
简单来说,这段代码会判断硬绑定函数是否是被new调用,若果是的话就会使用新创建的this替换硬绑定的this。而之所以要在new中使用硬绑定函数,主要目的是预先设置函数的一些参数,这样在使用new进行初始化时就可以只传入其余的参数。bind(..)的功能之一就是可以把除了第一个参数(第一个参数用于绑定this)之外的其他参数都直接传给下层的函数(这种技术成为“部分应用”,是“柯里化”的一种)。例:
function foo(p1, p2){ this.val = p1 + p2; } var bar = foo.bind(null, "p1"); //之所以使用null是因为在本例中并不关系硬绑定的this是什么,反正使用 new 时 this 会被修改 var baz = new bar("p2"); console.log(baz.val); //p1p2
2.3.4 优先级判断
现在可以根据优先级来判断函数在某个调用位置应用的是哪条规则,判断顺序如下:
- 函数是否在 new 中调用(new绑定)? 若是的话, this 绑定的是新创建的对象。例: var bar = new foo()。
-
函数是否通过call、apply(显示绑定)或者硬绑定调用? 若是的话,this 绑定的是指定的对象。例:var bar = foo.call(obj1)。
-
函数是否在某个上下文对象中调用(隐式绑定)? 若是的话,this 绑定的是那个上下文对象。例:var bar = obj1.foo()。
-
若以上情况都不是的话,则使用默认绑定:若在严格模式下就绑定到 undefined,否则就绑定到全局对象。例:var bar = foo()。
对于正常的函数调用来说,以上就是this的绑定原理了,但规则总有例外。
2.4 绑定例外
2.4.1 被忽略的this
如果把null或者undefined作为this的绑定对象传入call、apply或者bind,这些值在调用时会被忽略,实际上应用的时默认绑定规则:
/**非严格模式下**/
function foo(){
console.log(this.a);
}
var a = 2;
foo.call(null); //2
/**严格模式下**/
function foo(){
"use strict";
console.log(this.a);
}
var a = 2;
foo.call(null); //Uncaught TypeError: Cannot read property 'a' of null
一种常见的做法是使用apply(..)来“展开”一个数组,并当作参数传入一个函数。类似地,bind(..)可以对参数进行柯里化(预先设置一些参数)。这种方法有时非常有用:
function foo(a, b){
console.log(`a: ${a}, b: ${b}`)
}
//把数组“展开”成参数
foo.apply(null, [2, 3]); //a: 2, b: 3
//使用bind进行柯里化
foo.bind(null, 2)(3); //a: 2, b: 3
/**这两种方法都需要传入一个参数当作this的绑定对象,在函数不关心this的情况下,使用null是一个不错的选择**/
但总是使用null来忽略this可能会产生一些副作用。如果某个函数确实使用了this(比如第三方库中的一个函数),那默认绑定规则会把this绑定到全局对象(在浏览器中这个对象是window),这将导致不可预计的后果(比如修改全局对象)。一种更安全的做法是传入一个特殊的对象,把this绑定到这个对象不会对程序产生任何副作用,它是一个空的非委托对象,在JavaScript创建一个空对象最简单的方法就是Object.create(null),可以用任何名字来命名这个对象。这种情况下任何对于this的使用都会被限制在这个空对象中,不会对全局对象产生任何影响。例:
function foo(a, b){
console.log(`a: ${a}, b: ${b}`)
}
//创建空对象
var ø = Object.create(null); //可以任意命名表示空
//把数组“展开”成参数
foo.apply(ø, [2, 3]); //a: 2, b: 3
//使用bind进行柯里化
foo.bind(ø, 2)(3); //a: 2, b: 3
2.4.2 间接引用
另一个需要注意的是有可能在有意或者无意间创建一个函数的“间接引用”,在这种情况下,调用这个函数会应用默认绑定规则。间接引用最容易在赋值时发生:
function foo(){
console.log(this.a);
}
var a = 2,
o = {a: 3, foo: foo},
p = {a: 4};
o.foo(); // 3
(p.foo = o.foo)(); // 2
赋值表达式 p.foo = o.foo 的返回值是目标函数的引用,因此调用位置是 foo() 而不是p.foo()或者o.foo(),按照2.2的绑定规则,这里会应用默认绑定规则。
2.4.3 软绑定
硬绑定这种方式可以把this强制绑定到指定的对象(除了使用new时),防止函数调用时应用默认绑定规则。但硬绑定会大大降低函数的灵活性,使用硬绑定之后就无法使用隐式绑定或者显示绑定来修改this。而如果可以给默认绑定指定一个全局对象或者undefined以外的值,就可以实现和硬绑定相同的效果,同时保留隐式绑定或者显示绑定修改this的能力。可以通过一种被称为软绑定的方式实现这个期望:
if(!Function.prototype.softBind){
Function.prototype.softBind = function(obj){
//对指定的函数行径封装
var fn = this;
//捕获所有 curried 参数
var curried = [].splice.call(arguments, 1);
var bound = function(){
//若this绑定到全局对象或者undefined,则把指定的默认对象obj绑定到this,否则不修改this
return fn.apply((!this || this === (window || undefined)) ? obj : this, curried.concat.apply(curried, arguments));
};
bound.prototype = Object.create(fn.prototype);
return bound;
}
}
除了软绑定之外,softBind(..)的其他原理和ES5内置的bind(..)类似。这段代码还可支持可选的柯里化。
验证softBind(..)的软绑定功能:
function foo(){
console.log(`name: ${this.name}`);
}
var obj1 = {name: "obj1"},
obj2 = {name: "obj2"},
obj3 = {name: "obj3"};
var fooOBJ = foo.softBind(obj1);
fooOBJ(); // name: obj1
obj2.foo = foo.softBind(obj1);
obj2.foo(); // name: obj2 <-- 软绑定版本的 foo() 可以手动将this绑定到obj2上 - 隐式绑定
fooOBJ.call(obj3); // name: obj3 <-- 软绑定版本的 foo() 可以手动将this绑定到obj3上 - 显示绑定
setTimeout(obj2.foo, 10); // name: obj1 <-- 软绑定版本的默认绑定,将this绑定到了obj1上
2.5 this词法
2.2中介绍的四条绑定规则可以包含所有正常的函数,但ES6中出现的箭头函数并不能使用这些规则。箭头函数并不是使用function关键字定义的,而是使用被称为“胖箭头”的操作符 => 定义的。箭头函数不使用this的四种标准规则,而是根据外层(函数或者全局)作用域来决定this。例:
function foo(){
//返回一个箭头函数
return (a) => {
//this 继承自 foo()
console.log(this.a);
}
}
var obj1 = {
a: 1
}
var obj2 = {
a: 2
}
var bar = foo.call(obj1);
bar.call(obj2); // 1
/**代码运行结果可看出,箭头函数指向的是foo()的this**/
foo()内部创建的箭头函数会捕获调用时foo()的this。由于foo()的this绑定到obj1,bar(引用箭头函数)的this也会绑定到obj1,箭头函数的绑定无法被修改(new也不能修改)。
箭头函数最常用于回调函数中,例如时间处理器或者定时器:
function foo(){
setTimeout(() => {
//这里的this在词法上继承自foo()
console.log(this.a);
}, 100);
}
var obj = {
a: 0
}
foo.call(obj); // 0
箭头函数可以像bind(..)一样确保函数的this被绑定到指定对象,此外,其重要性还体现在它用更常见的词法作用域取代了传统的this机制。实际上,在ES6之前就已经在使用一种几乎和箭头函数完全一样的模式:
function foo(){
var _this = this;
setTimeout(() => {
//这里的this在词法上继承自foo()
console.log(_this.a);
}, 100);
}
var obj = {
a: 0
}
foo.call(obj); // 0
从本质上可以说,_this = this 和箭头函数都是想替代this机制。
关注:
- this既不指向函数自身也不指向函数的词法作用域。
- this是在运行时绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。
- this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
- 当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this就是记录的其中一个属性,会在函数执行的过程中用到。
- 调用位置:是函数在代码中被调用的位置,而不是声明的位置。
- 寻找调用位置就是寻找“函数被调用的位置”,最重要的是要分析调用栈(就是为了到达当前执行位置所调用的所有函数)。我们关心的 调用位置就在当前正在执行的函数的前一个调用中。
- 必须先找到调用位置,然后判断需要应用哪条规则,最终确定this的绑定对象。
-- 《你不知道的JavaScript》上卷 第二部分 第1,2章