你小子,又在偷偷学this指向
前言
- 在 深入理解V8执行流程、执行上下文和作用域! 中讲到,JavaScript每执行一段可执行代码时,都会创建相对于的执行上下文,对于每个执行上下文,都包含三个重要的属性:
- 词法环境 (
LexicalEnvironment
) 组件; - 变量环境 (
VariableEnvironment
) 组件; - 初始化
this
的值;
如果对作用域和执行上下文不太了解的同学可以看一下上面的提到的文章,这里讲述了 V8 的编译过程,以及作用域和执行上下文等令人难懂的概念,相信你阅读完会有很大的收获!
什么是this
- 与其他语言相比,函数的
this
关键字在JavaScript
中的表现略有不同,此外,在严格模式和非严格模式之间也会有一些差别。在全局上下文中,无论是严格模式或者非严格模式,this
都指向顶层对象(浏览器中是window)。 - 在绝大多数情况下,函数的调用方式决定了
this
的值(运行时绑定)。this
不能在执行期间被赋值,并且在每次函数被调用时this
的值也可能会不同。 this
是在运行时绑定的的,并不是在编写时绑定的,它的执行上下文取决于函数调用时的各种条件。this
的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
绑定规则
默认绑定
- 首先要介绍的是最常用的函数调用类型:
独立函数调用
,思考以下代码:
function f00() {
console.log(this.a);
}
var a = 2;
foo(); // 2
- 在开头提到的那篇文章中说过,在全局作用域下用
var
关键字声明的变量和在全局声明的函数
会被挂载到全局对象(window
)上。 - 当我们看到调用
foo()
时,我们都知道,全局声明的函数的作用域是顶层的globalObject
在浏览器中也就是window
。 - 通过观察,我们可以看出,在代码中,
foo()
是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,所以函数中的this
为window
,也就是window.a
,所以自然而然的就输出 2 了。 - 如果使用严格模式
strict mode
,则不会将全局对象用于默认绑定,因为this
会绑定到undefined
;
function f00() {
"use strict";
console.log(this.a);
}
var a = 2;
f00(); // Cannot read properties of undefined (reading 'a')
// 因为严格默认情况下,默认绑定,this会被绑定为 undefined ,所以this.a也就等于undivided.a
// 因为 undefined 下没有 a 的属性,所以会报类型错误
- 值得注意的是,如果
foo()
运行在非 strict mode 下时,默认绑定才能绑定到全局对象,在严格模式foo()
则不影响默认绑定。
function f00() {
console.log(this.a);
}
var a = 2;
(function () {
"use strict";
f00(); // 2
})();
隐式绑定
- 隐式绑定的规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含,但是这样的说法可能不太会,先来思考下面的代码:
function foo() {
console.log(this.a);
}
var obj = {
a: 111,
foo,
};
obj.foo(); // 111
- 首先需要注意的是
foo()
的声明方式,以其之后是如何被当做意引用属性添加到abj
对象中的。但是无论是直接在obj
中定义还是先定义再添加为引用属性,这个函数严格来说都不属于obj
对象。 - 然而调用位置会使用
obj
上下文来引用函数,因此你可以说函数被调用时obj
对象 "拥有" 或者 "包含" 函数引用。 - 当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。因此
this.a
和obj.a
是一样的。
- 对象属性引用链只有上一层或者说最后一层在调用位置中起作用,例如
function foo() {
console.log(this.a);
}
var obj2 = {
a: 111,
foo,
};
var obj1 = {
a: 777,
obj2,
};
obj1.obj2.foo(); // 111
// 对象 obj2 为最后一层
// obj1.obj2 仅为属性查找,并还没有开始调用
函数脱离原上下文
- 一个最常见
this
绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说他会应用默认绑定默认。
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo,
};
var bar = obj.foo; // 函数别名
var a = "我是window下的a";
bar(); // 我是window下的a
- 虽然
bar
是obj.foo
的一个引用,但是实际上,它引用的是foo
函数的本身,因此此时的bar()
其实是一个普通的函数调用 因此应用了默认绑定。 - 这实际上是重新定义了一个
bar
函数,和对象的结构一样,都是重新赋值,参考一下代码:
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo,
};
var { foo } = obj; // 这里相当于重新定义了一个函数或者说这是一个函数别名
var a = "我是window下的a";
foo(); // 我是window下的a
var object = {
moment: 777,
age: 18,
};
console.log(object); // {moment: 777, age: 18}
var { moment } = object;
moment = "牛逼";
console.log(moment); // 牛逼
console.log(object); // {moment: 777, age: 18}
- 上面的代码,解构出来的变量
moment
,实际上在全局作用域中创建了一个变量moment
并赋值为777
,后面的直接修改变量不修改对象object
中的属性moment
。
函数作为参数
function foo() {
console.log(this.a);
}
function bar(fn) {
// fn 其实是引用 foo
fn();
}
var obj = {
a: 777,
foo,
};
var a = "牛逼啊,这也行";
bar(obj.foo); // 牛逼啊,这也行
- 参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,上面这段代码实际上就是以下代码的变体:
function foo() {
console.log(this.a);
}
function bar() {
const fn = obj.foo;
fn();
}
var obj = {
a: 777,
foo,
};
var a = "牛逼啊,这也行";
bar(); // 牛逼啊,这也行
显示绑定
- 在
JavaScript
中,无论是宿主环境提供的一些函数还是你自己创建的函数,你都可以使用call(...)
和apply(...)
方法。 - 他们的第一个参数是一个对象,是给this准备的,接着在调用函数时将其绑定到
this
。因为你可以直接指定this
的绑定对象,因此我们称之为显示绑定
、 - 这里 apply 和 call的语法规则就不讲了,有需要的可以去
mdn
官网查阅。
硬绑定
硬绑定
这种方式可以把this
强制绑定到指定的对象 (new
除外),既然有硬绑定
,自然也有软绑定
,在后文中我们会讲到。
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
};
var bar = function () {
foo.call(obj);
};
bar(); // 2
setTimeout(bar, 1000); // 2
// 硬绑定的 bar 不可能再修改他的 this
bar.call(window); // 2
- 用
apply
方法也一样的结果,只不过参数参数的方式不一样。 - 而
bind
方法会返回一个硬编码的新函数,它会把你指定的参数设置为this
的上下文调用原始参数。
API调用的 "上下文"
JavaScript
语言和宿主环境
提供了许多内置函数,都提供了一个可选的参数,通常成为上下文
,其作用和bind(...)
一样,确保你的回调函数使用指定的this
。
function callback(element) {
console.log(element, this.id);
}
var obj = {
id: "真不错",
};
// 调用 foo(...) 时把 this 绑定到 obj 上
[1, 2, 3].forEach(callback, obj);
// 1 '真不错' 2 '真不错' 3 '真不错'
// 俺 map 也一样
[1, 2, 3].map(callback, obj);
// 1 '真不错' 2 '真不错' 3 '真不错'
new绑定
- 在开始讲绑定之前,我想你已经知道了使用
new
来调用构造函数会执行什么操作,我们就再回顾一下吧:
- 在内存中创建一个新对象;
- 这个新对象内部的
[[prototype]]
特性 被赋值为构造函数的prototype
属性 (如果不了解这个也可以 点击这里); - 构造函数中内部的
this
被赋值为这个新对象(即this
指向新对象); - 执行构造函数内部的代码(给新对象添加属性);
- 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象;
function Foo(moment) {
this.moment = moment;
}
var bar = new Foo(777);
console.log(bar.a); // 777
- 使用
new
来调用Foo(...)
时,我们会构造一个新对象并把他绑定到Foo(...)
调用中的this
上。 - 我们再来思考一下的代码输出结果是什么:
var mayDay = {
moment: "moment",
};
function Foo() {
this.moment = 777;
return mayDay;
}
var bar = new Foo();
console.log(bar.moment);
- 最终输出的结果是
moment
,也就是this
被绑定到了mayDay
对象上,那么为什么会这样呢?
答案就在 new 的最后一条过程 "如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象" 这条规则上。
- 换句话说就是,如果构造函数返回一个对象,则该对象将作为整个表达式的值返回,而传入的构造函数的
this
将会被抛弃。 - 如果构造函数返回的是非对象类型,则忽略返回值,返回新创建的对象
var mayDay = {
moment: "moment",
};
function Foo() {
this.moment = 777;
return 111; // 这里的返回值变化了
}
var bar = new Foo();
console.log(bar.moment); // 777 输出的是新对象的 moment
类上下文
this
在 类中的表现与函数中类似,因为类本质上也是函数,但也有一些区别和注意事项。在类的构造函数中,this
是一个常规对象。类中所有非静态的方法都会被添加到this
的原型中:
class Example {
constructor() {
const proto = Object.getPrototypeOf(this);
console.log(Object.getOwnPropertyNames(proto));
}
first() {}
second() {}
static third() {} // 这里不在 this 上,在类本身上
}
new Example(); // ['constructor', 'first', 'second']
箭头函数调用
箭头函数表达式的语法比函数表达式更简洁,并且没有自己的
this
,arguments
,super
或new.target
。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。正是因为箭头函数没有this
,自然而然的就不能使用new
操作符了。
var moment = "moment";
var bar = {
moment: 777,
general: function () {
console.log(this.moment);
},
arrow: () => {
console.log(this.moment);
},
nest: function () {
var callback = () => {
console.log(this.moment);
};
callback();
},
};
bar.general(); // 777
bar.arrow(); // moment
bar.nest(); // 777
- 其中第一个普通函数的就是我们前面说的隐式绑定。
- 第二个调用因为箭头函数没有自己的
this
,他会查找箭头函数上一层的的普通函数的this
,这时演变成了默认绑定了,是全局调用。 - 第三个和第二个相似,但是它查找的上一层是函数
nest
,这是一个隐式绑定了,自然也就输出对象内部的monent
。 - 虽然箭头函数无法通过
call
,applu
,bind
绑定this
,但是他可以绑定缓存箭头函数上层的普通函数的this
,例如:
var foo = {
moment: 777,
general: function () {
console.log(this.moment);
return () => {
console.log("arrow:", this.moment);
};
},
};
var obj = {
moment: "moment",
};
foo.general().call(obj); // 777 "arrow: 777 "
foo.general.call(obj)(); // 'moment' 'arrow:' 'moment'
- 注意
settimeout
和自执行函数
中的this
指向window
setTimeout(function foo() {
console.log(this); // window
}, 0);
(function () {
console.log(this); // window
})();
- 因为
settimeout
这个方法是挂载在window
对象上的,settimeout
执行时,执行回调中的this
指向调用settimeout
的对象,所以是window
。
优先级
- 如果某个调用位置可以应用多条规则该怎么办?为了解决这个问题就必须给这些规则设定优先级。显而易见,默认绑定的优先级是四条规则中最低的。
function foo() {
console.log(this.a);
}
var obj1 = {
a: 666,
foo,
};
var obj2 = {
a: 777,
foo,
};
obj1.foo(); // 666
obj2.foo(); // 777
obj1.foo.call(obj2); // 777
obj2.foo.call(obj1); // 666
- 通过以上代码可以看到,
显示绑定
比隐式绑定
优先级更高,也就是说在判断是应当先考虑是否可以存在显示绑定。
function foo(age) {
this.age = age;
}
var obj1 = {
foo,
};
var obj2 = {};
obj1.foo(2);
console.log(obj1.age); // 2
obj1.foo.call(obj2, 3);
console.log(obj2.age); // 3
var bar = new obj1.foo(7);
console.log(obj1.age); // 2
console.log(bar.age); // 7
-
可以看到
new绑定
比隐式绑定
优先级更高,但是new绑定
和显示绑定
谁的优先级更高呢? -
因为
new
和call/apply
无法一起使用,因此无法通过new foo.call(...)
来直接测试,但是我们可以使用硬绑定来测试他俩的优先级。
function foo(age) {
this.age = age;
}
var obj1 = {};
var bar = foo.bind(obj1);
bar(2);
console.log(obj1.age); // 2
var baz = new bar(3);
console.log(obj1.age); // 2
console.log(baz.age); // 3
- 结果出乎意料,
bar
被绑定到obj1
上,但是new bar(3)
并没有像我们语句的那样把obj1.age
修改为3
。相反,new
修改了硬绑定 (到obj1
的) 调用bar(...)
中的this。 - 这是因为
new
调用时bind之后的函数,会忽略bind
绑定的第一个参数,稍后我们会用bind
方法的ployfill
实现来讲清楚为什么会这样。 - 综上所述,它们的优先级顺序分别是:
new
调用;call
、apply
、bind
调用;- 隐式绑定(对象方法调用);
- 默认绑定(普通函数调用);
bind的ployfill实现
Function.prototype.Bind = function (pointer) {
if (typeof this !== "function") {
throw new TypeError(
"Function.prototype.bind - what is trying to be bound is not callable"
);
}
// 将参数转换为数组
const args = Array.prototype.slice.call(arguments, 1);
const self = this;
const NewFunc = function () {};
const fBound = function () {
return self.apply(
// 如果是 new 操作符,则重新绑定this
this instanceof NewFunc && pointer ? this : pointer,
args.concat(Array.prototype.slice.call(arguments))
);
};
NewFunc.prototype = this.prototype;
fBound.prototype = new NewFunc();
return fBound;
};
- 其中,下面就是
new
修改this
的相关代码:
this instanceof NewFunc && pointer ? this : pointer;
// ... 以及;
NewFunc.prototype = this.prototype;
fBound.prototype = new NewFunc();
软绑定
- 之前我们讲到,硬绑定这种方法可以把
this
强制绑定到指定的对象(除了使用new
时),防止函数调用应用默认绑定规则。 - 但是问题就在于硬绑定会大大降低函数的灵活性,使用硬绑定之后就无法使用隐式绑定或者显示绑定来修改
this
的能力,具体来看实现:
Function.prototype.softBind = function (object) {
let fn = this;
// 捕获所有的curried参数
const curried = [].slice.call(arguments, 1);
const bound = function () {
return (
fn.apply(!this || this === (window || global) ? object : this),
curried.concat.apply(curried, arguments)
);
};
bound.prototype = Object.create(fn.prototype);
return bound;
};
function foo() {
console.log(this.name);
}
const obj = {
name: "obj",
};
const obj2 = {
name: "obj2",
};
const obj3 = {
name: "obj3",
};
const fooOBJ = foo.softBind(obj);
fooOBJ(); // obj
obj2.foo = foo.softBind(obj);
obj2.foo(); // obj2
fooOBJ.call(obj3); // obj3
setTimeout(obj2.foo, 1000); // obj
- 可以看到,软绑定版本的
foo()
可以手动的将this
绑定到不同的对象上。
参考文章
- 书籍 你不知道的JavaScript 上卷
- MDN
结尾
- 一个小小的
this指向
,就涵盖了new、call、apply、bind
,箭头函数等用法。从而扩展到作用域、闭包,原型链,继承、严格模式,这实力不容小觑。
转载自:https://juejin.cn/post/7162747517350707213