注册

JS中this的指向原理

前言


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


调用位置



理解调用位置:调用位置就是函数在执行时被调用的位置(而不是声明的位置)。



要找到函数的调用位置,最重要是找到函数的调用栈(就是为了到达当前执行位置所调用的所有函数),而函数的调用位置就是当前所在栈顶的前一个位置。


举个栗子


function baz() { 
// 当前调用栈是:baz
// 因此,当前调用位置是全局作用域,浏览器下位window,node下为global
console.log( "baz" );
bar(); // <-- bar 的调用位置
}
function bar() {
// 当前调用栈是 baz -> bar
// 因此,当前调用位置在 baz 中
console.log( "bar" );
foo(); // <-- foo 的调用位置
}
function foo() {
// 当前调用栈是 baz -> bar -> foo
// 因此,当前调用位置在 bar 中
console.log( "foo" );
}
baz(); // <-- baz 的调用位置

this绑定规则


函数的this在js引擎执行时,会根据一些规则去绑定到上下文中。


默认绑定


默认绑定应用在最常用的函数调用类型:独立函数调用上。可以把这条规则看作是无法应用其他规则时的默认规则。


function foo() {
//默认规则下,this指向全局对象,即顶层作用域
console.log( this.a );
}

var a = 2;
foo()//2

怎么知道这里应用了默认绑定呢?可以通过分析调用位置来看看 foo()是如何调用的。在代码中,foo() 是直接使用不带任何修饰的函数引用进行调用的,因此只能使用
默认绑定,无法应用其他规则。



严格模式下,不能将全局对象用于默认绑定,因此 this 会绑定到undefined,在浏览器和node中是一样的。



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


function foo() { 
//在非严格模式下运行
console.log( this.a );
}
var a = 2;
(function(){
//在严格模式下调用
"use strict";
foo(); // 2
})();

以上代码混合使用了严格模式和非严格模式,因此foothis不受严格模式影响,但混合使用严格模式是不提倡的,幸运的是es6默认是严格模式


隐式绑定


当一个函数的引用被一个对象持有时(作为该对象的方法),那么该函数的this就绑定在了这个对象上。通常这在声明一个对象,并将一个已声明的函数作为该对象属性时触发。


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

obj对象声明时,foo作为obj的一个属性,因此其this被隐式绑定到了obj上,因为obj持有对foo的引用。



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



举个栗子


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

// obj2.foo引用了foo函数
var obj2 = {
a: 42,
foo: foo
};

//obj1.obj2 引用了obj1对象
var obj1 = {
a: 2,
obj2: obj2
};

//但是foo中的this永远指向直接持有它的引用的那个对象,即obj2
obj1.obj2.foo(); // 42

一个函数的引用被一个对象持有,而这个对象的引用又被另一个对象持有,另一个对象的引用再被另一个对象持有...,这就像一条项链,但是不管层次有多深,这个函数的this永远指向直接持有它的引用的那个对象。


隐式丢失



一个最常见的 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"

虽然barobj.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"

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


如果把函数传入语言内置的函数而不是传入你自己声明的函数,会发生什么呢?结果是一
样的,没有区别:


function foo() { 
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // a 是全局对象的属性
setTimeout( obj.foo, 100 ); // "oops, global"

以上的栗子再次向我们证明了,函数this是在运行时绑定的,与声明位置无关。



除此之外,还有一种情



况 this 的行为会出乎我们意料:调用回调函数的函数可能会修改 this。在一些流行的
JavaScript 库中事件处理器常会把回调函数的 this 强制绑定到触发事件的 DOM 元素上。如onclick,addEventListener,会将this绑定在dom元素 上。


显式绑定


显示绑定就是利用js提供的一些内置函数,将this绑定到指定的上下文中。



具体点说,可以使用函数的 call(..) 和



apply(..) 方法。严格来说,JavaScript 的宿主环境有时会提供一些非常特殊的函数,它们
并没有这两个方法。但是这样的函数非常罕见,JavaScript 提供的绝大多数函数以及你自
己创建的所有函数都可以使用 call(..) 和 apply(..) 方法。


function foo() { 
console.log( this.a );
}
var obj = {
a:2
};
//执行时,foo的this就是obj了
foo.call( obj ); // 2


如果你传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作 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

很好理解,就是在函数运行时再把这个函数绑定到我们制定的this上。



硬绑定的典型应用场景就是创建一个包裹函数,负责接收参数并返回值



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

由于硬绑定是一种非常常用的模式,所以 ES5 提供了内置的方法 Function.prototype.bind,bind(..) 会返回一个硬编码的新函数,它会把你指定的参数设置为 this 的上下文并调用原始函数


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

API中可选的调用“上下文”



第三方库的许多函数,以及 JavaScript 语言和宿主环境中许多新的内置函数,都提供了一



个可选的参数,通常被称为"上下文"(context),其作用和 bind(..) 一样,确保你的回调函数使用指定的 this。这些函数实际上就是通过 call(..) 或者 apply(..) 实现了显式绑定。


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

new 绑定


使用new来调用函数时(函数也是对象),或者说发生构造函数调用时,会自动执行下面的操作:



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

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


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



规则的优先级


实际判断时,一个场景可能存在多个规则,因此判定时需要由高优先级往下判定。


可以按照下面的顺序来进行判断:




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




  2. 函数是否通过callapply(显式绑定)或者硬绑定调用?如果是的话,this 绑定的是




指定的对象。



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

下文对象。



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

到全局对象。


规则例外



在某些场景下this的绑定行为会出乎意料,你认为应当应用其他绑定规则时,实际上应用的可能是默认绑定规则。



1. 将null或undefined作为this进行显式绑定


2. 赋值表达式的返回值


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()。根据我们之前说过的,这里会应用默认绑定。


3.软绑定


硬绑定很好地解决了隐式绑定可能会无意间将this绑定在顶级作用对象(严格模式下,为undefined)上的问题,但降低了其灵活性,我们要的结果是,保留其灵活性,既能绑定到指定的this上,但又不想让它默认绑定到全局对象上,解决方法就是软绑定。


通俗的说,就是有一个默认值,指定了绑定对象的话就绑定到指定的对象上,否则就绑定到默认对象。


//实现和硬绑定相同的效果,同时保留隐式绑定或者显式绑定修改 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;
};
}

总结


如果要判断一个运行中函数的 this 绑定,就需要找到这个函数的直接调用位置。


找到之后就可以顺序应用下面这四条规则来判断 this 的绑定对象。



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

箭头函数不会以上四条标准的绑定规则,而是根据当前的词法作用域来决定this,具体来说,箭头函数会继承外层函数调用的 this 绑定(无论this绑定到什么)。这和我们创建一个变量来保存当前的this的效果是一样的。


链接:https://juejin.cn/post/7000756069244862477

0 个评论

要回复文章请先登录注册