每日读《你不知道的JavaScript(上)》| this进阶
前言
this 基础打完,该深入了解 this 了。
毕竟,看完前面的 this 基础,还不足以能够在项目中熟练运用 this。
真正的重点,this 指向问题还没完呢。
因为 this 指向是能够拆分成很多个小 part 深入了解的。
所以接下来,就一起来探索吧!
分析调用位置
调用位置决定 this 的绑定对象,所以我们分析调用位置是非常必要的。
给段代码,举例说明:
function outer() {
console.log('outer');
middle();
}
function middle() {
console.log('middle');
inner();
}
function inner() {
console.log('inner');
}
outer();
调用链显而易见:
outer --> middle --> inner
比如 outer 的调用位置在全局。
middle 的调用位置在 outer。
调用位置是当前函数的上一层
用控制台去分析也是一样的。
我们在代码段的第一行插入 debugger。
拿 middle 举例,执行到 middle 中的语句时,如下图所示:
运行代码时,调试器会在那个位置暂停,同时会展示当前位置的函数调用列表。
而栈中的第二个元素就是我们要找的调用位置。
绑定的规则
在了解函数所在调用位置是如何绑定 this 的之前,我们需要了解常见的绑定规则。
主要有四大规则:
默认、隐式、显示、new 绑定
默认绑定
这种绑定发生在独立函数调用的情况下。
就是说,没有依托任何对象,直接调用函数的情况。
such as:
function fn() {
console.log(this.a);
}
var a = 1;
fn(); // 1 -- 这种就属于是独立函数调用
但有个容易被忽略的前提啊,得在非严格模式下,才会进行默认绑定。
不然会报错。
隐式绑定
这种情况发生在调用位置含有上下文对象中。
就是这个函数会被某些对象所引用。
比如:
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2 -- 这意味着 foo 被 obj 所引用
此时 foo 中的 this 会隐式地被绑定到 obj 对象上。
这个不难理解。
⚠️但是隐式绑定有一些问题需要格外注意⚠️这些例子也是面试中会常考的点。
NO.1
如果存在链式调用的情况,this 如何绑定?
就是比如obj1.obj2.foo()
的情况下,this会指向谁呢?
那我们写一段代码验证一下:
function foo() {
console.log(this.name);
}
var obj1 = {
name: 'Judy',
obj2
};
var obj2 = {
name: 'Males',
foo: foo
};
obj1.obj2.foo(); // Males
woc报错了,打开新的标签页,第一次执行报错,同个标签内再次执行正常打印结果。
(写个例子我也不知道会报错啊...😫😫)
就地解决吧...找找为啥第一次报错了。
我试了一下,把定义 obj1 和 obj2 的位置换一下,变成这样:
function foo() {
console.log(this.name);
}
var obj2 = {
name: 'Males',
foo: foo
};
var obj1 = {
name: 'Judy',
obj2
};
obj1.obj2.foo();
就不会报错了。
既然换位置能解决问题,那么涉及到声明的问题,会不会是和变量提升有关系呢?
奥,研究了一下,我画个图:
原来是因为这个时候obj1.obj2
的属性值是 undefined,然后我调用undefined.foo()
就抛出了一个 TypeError 的错误!
正确的执行顺序图解:
明白了之后,我们回归正题,继续探究 this 指向问题。
在obj1.obj2.foo()
这里很明显 this 是指向 obj2 的。
原文中是这样的描述规则的:
对象属性引用链中只有最顶层或者说最后一层会影响调用位置。
其实简单来说,哪怕在很长的对象引用链中,只看.foo()
前面的对象就可以了,就是函数 foo 内部的 this 指向。
NO.2
这种情况属于隐式丢失,就是本身可能绑定到某个具体的对象上,然后转移成默认绑定。
可以理解为 this 指向变化了,而且是变成默认绑定。
举个例子:
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // 看这里!
var a = 'hello!!';
bar();
这里关键在于我们给 obj 的 foo 函数赋了一个新的别名 bar。
这个行为会导致 this 指向隐式丢失。
因为一旦赋予了这个别名,就意味着bar 引用的是 foo 函数本身。
然后函数本体在全局环境下调用,没有任何其他的修饰,因此 this 指向就进行默认绑定了。
显示绑定
显示绑定其实就是通过一些手段直接强制改变 this 指向。
方法学过前端的同学都知道,call、apply和bind。
最常考的也是这三者之间的区别和 bind 的实现。
call 和 apply 的区别比较简单,在于入参不同。
具体用法去MDN补,这篇字数有点多了,不多做详细介绍。
关键是 bind 啊 bind!
书中其实提到一个概念,叫硬绑定。
硬绑定就是能够避免隐式丢失的绑定方式,意味着硬绑定之后不可能再修改它的 this。
bind 就属于硬绑定很好的一种方案。
先说硬绑定的实现方式:
var a = 1; // 全局来个 a
function foo() {
console.log(this.a);
}
var obj = {
a: 2
};
var bar = function() {
foo.call(obj); // 请关注这里,关键点哦
}
bar();
setTimeout(bar, 1000); // 2
bar.call(window); // 2 -- 再次改变 this 指向,失败
显而易见,硬绑定的方法就是新建一个函数,然后在这个新建的函数里面绑定this的最终指向。
看完硬绑定,妈妈再也不用担心我手写 bind 了。
(要是被有的面试官看到估计又要拷我了)
bind 就是把类似 bar 这种函数封装起来,然后作为匿名函数 return 出去。
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
可能有些小朋友不记得 bind 的用法了,稍微回顾一下下。
用法:函数.bind(this要指向的对象)
简单理解一下如何手写bind:
function foo(num) {
console.log(this.a, num); // 这里log出来能更清晰地看到this指向
return this.a + num;
}
// 简单版手写bind
function bind(fn, obj) {
return function() { // bind的核心是创建了一个新的函数
return fn.apply(obj, arguments);
};
}
var obj = {
a: 2
};
var bar = bind(foo, obj); // 新建bar变量用来接收bind返回的新函数
var b = bar(3); // 2 3
console.log(b); // 5
最后我们来总结一下隐式丢失和硬绑定的区别:
var bar = obj.foo; // 隐式丢失
var bar = function() { foo.call(obj); } // 硬绑定
可以看到调用 bar 的时候,this的指向完全不同了。
前者obj.foo
属于普通函数在全局情况下调用,this会执行默认绑定。
后者哪怕调用 bar 内部会用 call 显示地把 this 指向 obj。
new 绑定
讲这个 new 绑定之前,先回顾一下 new 的用法:
let 实例对象 = new 构造函数/类();
new 一个对象的时候做了哪些事情?
-
创建一个全新的对象
-
这个新对象内部的
[[Prototype]]
特性被赋值为构造函数的prototype
属性 -
构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)
-
执行构造函数内部的代码(给新对象添加属性)
-
如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象
比如手写一个简易版的new,来理解上述流程:
function customNew(constructor,...args) {
//创建一个空对象,继承 constructor 的原型
const obj = Object.create(constructor.prototype); //相当于赋值到obj这个空对象的prototype原型对象上
//将obj作为this,执行 constructor,传入参数
constructor.apply(obj,args);
//返回obj
return obj;
}
大家不理解Object.create()
的可以去MDN查一下。
然后我们再来看 new 绑定:
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log( bar.a ); // 2
使用 new 来调用 foo 时,我们会构造一个新对象并把它绑定到 foo 调用中的 this 上。
优先级
默认绑定、隐式绑定、显示绑定和 new 绑定这四种之间不用说都有个优先级,但我们一般在项目中会选择性地写,不会把这些都写一块儿,所以我就简略地介绍一下。
显示 > 隐式
new > 隐式
new > 显示
所以,new > 显示 > 隐式。
这对于判断 this 有一定的帮助,不过还有一些例外的情况。
小结
本章主要介绍了 this 指向问题的核心,如何分析调用位置和绑定规则,只有牢牢掌握了这两个问题,在正式判断 this 指向的时候,才能减少错误率!
还讲解了绑定的规则,各显神通,有些需要额外关注的比如bind的原理,都是能够学以致用的知识呢!
最后说一句,很久没更文了,其实大部分已经写好了,断断续续补一点,没发出来,hhh,最近都在忙毕业啊答辩啊啥的事情,没空写博客。
我努力QAQ。尽量挤点时间出来更文。干巴爹!!!
转载自:https://juejin.cn/post/7374238289107124263