LexicalEnvironment
) 组件;VariableEnvironment
) 组件;this
的值;如果对作用域和执行上下文不太了解的同学可以看一下上面的提到的文章,这里讲述了 V8 的编译过程,以及作用域和执行上下文等令人难懂的概念,相信你阅读完会有很大的收获!
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.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
的绑定对象,因此我们称之为 显示绑定
、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
来调用构造函数会执行什么操作,我们就再回顾一下吧:[[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
调用;-
- 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
绑定到不同的对象上。