前语
- 在 深入理解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()
的声明办法,以其之后是如何被当做引证特点添加到obj
目标中的。可是无论是直接在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
,箭头函数等用法。然后扩展到效果域、闭包,原型链,承继、严厉形式,这实力不容小觑。