this指向问题全面解析

本文详细介绍了JavaScript中this的四种绑定规则:默认绑定、隐式绑定、显示绑定和new绑定,以及它们的优先级。通过实例展示了如何在不同场景下确定this的指向,特别提到了箭头函数不使用常规的this规则。此外,还讨论了硬绑定和软绑定的概念,帮助读者深入理解JavaScript中的函数调用和作用域。

1.1 为什么要用this

1.2 误解

1.2.1 指向自身

function foo(num) { 
 console.log( "foo: " + num ); 
 // 记录 foo 被调用的次数
 this.count++; 
} 
foo.count = 0; 
var i; 
for (i=0; i<10; i++) { 
 if (i > 5) { 
 foo( i ); 
 } 
} 
// foo: 6 
// foo: 7 
// foo: 8 
// foo: 9 
// foo 被调用了多少次?
console.log( foo.count ); // 0 -- 什么?!
console.log(count); // NAN  没报错

foo函数调用了4次,但foo.count仍然为0, 实际上我们是在全局作用域下创建了count这个变量,但并未赋值,所以是NAN,即此时foo里面的this不是指向自身,而是指向全局window

1.2.2 指向它的作用域

function foo() { 
 var a = 2; 
 this.bar(); 
} 
function bar() { 
 console.log( this.a ); 
} 
foo(); // ReferenceError: a is not defined

报错,说明this不指向它的作用域,所以不能通过作用域来查找或者将this跟作用域联系起来使用

1.3 this到底指向什么

  • this是在运行时才被绑定的
  • this的绑定和函数声明的位置没有关系,只取决于函数的调用方式
  • 当一个函数被调用时,会创建一个活动记录(执行上下文),this就是这个记录的一个属性,会在函数执行过程中用到

第二章 this全面解析

2.1 绑定规则

2.1.1 默认绑定

当函数是直接使用且不带任何修饰时(独立函数调用),会使用默认绑定规则

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

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

如果使用严格模式,则不能将全局对象用于默认绑定,因此this会绑定到undefined

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

这里有一个比较重要的细节,虽然this的绑定规则完全取决于调用位置,但是只有foo()运行在非严格模式下时,默认绑定才能绑定到全局对象,在严格模式下调用foo()则不影响默认绑定

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

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

2.1.2 隐式绑定

这条规则则是需要考虑函数调用位置是否有上下文对象,

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

调用位置会是用obj的上下文来引用函数,当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象中

对象属性链中只有上一层或者最后一层在调用位置中起作用,如

function foo() { 
 console.log( this.a ); 
} 
var obj2 = { 
 a: 42, 
 foo: foo 
}; 
var obj1 = { 
 a: 2, 
 obj2: obj2 
}; 
obj1.obj2.foo(); // 42
隐式丢失

即被绑定的函数会丢失绑定对象,就会应用默认绑定

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"

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

个例子一样

2.1.3 显示绑定

直接指定this的绑定对象,称为显示绑定,例如可以将函数用call或apply指定this的绑定对象

call和apply的使用
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 );  // 强制把foo的this绑定在obj上
                     }; 
bar(); // 2 setTimeout( bar, 100 ); // 2 // 硬绑定的 bar 不可能再修改它的 this 
 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; 
} // 简单的辅助绑定函数
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

2.1.4 new绑定

使用new来调用函数时,会自动执行下面的操作:

  • 创建或者说构造一个新对象
  • 这个新对象会执行[[prototype]]连接
  • 这个新对象会绑定到函数调用的this
  • 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象
function foo(a) {  
    this.a = a; 
} 
var bar = new foo(2); 
console.log( bar.a ); // 2

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

2.2 优先级

现在可以通过判断函数的调用位置来判断应用哪一条规则,但如果同时可以应用多条规则的话,就需要知道他们的优先级

隐式绑定和显式绑定比较
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 
// 隐式绑定与new绑定的比较
var bar = new obj1.foo( 4 );  // 即判断此时的this指向谁,
console.log( obj1.a ); // 2 
console.log( bar.a ); // 4

可以看到 new 绑定比隐式绑定优先级高

new绑定与显示绑定的比较
new foo.call(obj1)  // TypeError: foo.call is not a constructor

new 和 call/apply 无法一起使用,因此无法通过 new foo.call(obj1) 来直接进行测试。但是我们可以使用硬绑定来测试它俩的优先级。

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。因此,new绑定的优先级高于显示绑定

总结
  • 函数是否在 new 中调用(new 绑定)?如果是的话 this 绑定的是新创建的对象。var bar = new foo()

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

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

  • 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined,否则绑定到全局对象。var bar = foo()

软绑定
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 箭头函数

箭头函数不使用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 !
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值