likes
comments
collection
share

每日读《你不知道的JavaScript(上)》| this进阶

作者站长头像
站长
· 阅读数 24

前言

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 中的语句时,如下图所示:

每日读《你不知道的JavaScript(上)》| this进阶

运行代码时,调试器会在那个位置暂停,同时会展示当前位置的函数调用列表。

而栈中的第二个元素就是我们要找的调用位置。

绑定的规则

在了解函数所在调用位置是如何绑定 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报错了,打开新的标签页,第一次执行报错,同个标签内再次执行正常打印结果。

每日读《你不知道的JavaScript(上)》| this进阶

(写个例子我也不知道会报错啊...😫😫)

就地解决吧...找找为啥第一次报错了。

我试了一下,把定义 obj1 和 obj2 的位置换一下,变成这样:

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


var obj2 = {
    name: 'Males',
    foo: foo
};


var obj1 = {
    name: 'Judy',
    obj2
};

obj1.obj2.foo();

就不会报错了。

每日读《你不知道的JavaScript(上)》| this进阶

既然换位置能解决问题,那么涉及到声明的问题,会不会是和变量提升有关系呢?

奥,研究了一下,我画个图:

每日读《你不知道的JavaScript(上)》| this进阶

原来是因为这个时候obj1.obj2的属性值是 undefined,然后我调用undefined.foo()就抛出了一个 TypeError 的错误!

正确的执行顺序图解:

每日读《你不知道的JavaScript(上)》| this进阶

明白了之后,我们回归正题,继续探究 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 了。

(要是被有的面试官看到估计又要拷我了)

每日读《你不知道的JavaScript(上)》| this进阶

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 一个对象的时候做了哪些事情?

  1. 创建一个全新的对象

  2. 这个新对象内部的[[Prototype]]特性被赋值为构造函数的prototype属性

  3. 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)

  4. 执行构造函数内部的代码(给新对象添加属性)

  5. 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象

比如手写一个简易版的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
评论
请登录