What's "this"?握住"this"!!

前言

this机制,我猜大部分的JavaScript使用者都对它头疼不已,它确实在很多时候能给我们写代码带来便利,但有些时候却让人难以掌控,时常会出现指哪哪不灵的场景。作为受害者之一,出于好奇和对代码的控制欲,我不得不发挥一下强迫症的力量,驱使我弄懂它,现分享一下我对this关键字的理解。


一、this关键字是什么?

  • thisjavascript中最复杂的机制之一,this关键字存在于我们声明的函数中,它是我们所期望操作对象的代表。
  • 当一个函数被调用时,会在编译器中留下一条记录,这个记录包括了函数的调用位置、调用方式、传递的参数等信息,this也作为记录中的一个信息属性在函数执行的时候被使用。

二、this关键字指向哪里?

在我初次接触this的时候,一眼就认定了它指向的是函数本身,不知屏幕面前的你是否和我一样有同样的经历?我猜也有和我有不同想法的人,是不是认为this指向函数的作用域呢?这很复杂,在某种情况下这种猜想是正确的。现在我们一起抓住傀儡背后的幕后黑手。

2.1、两个误区

  • 误区1: 指向函数本身, 这是最容易让人误解的情况,那我们看看是不是和我们想的一样:

    javascript 复制代码
    function st(){
        this.a = 1;
    }
    st();
    console.log(st.a); // undefined 
    console.log(window.a); // 1 

    我在全局声明了一个st函数,调用它后,没有按照函数字面意图给函数st添加一个属性a,反而在window对象中我们找到了那个a属性。

  • 误区2: 指向函数本身的作用域 在某种情况下是正确的,在上面的例子中确实是指向了st函数的作用域,也就是全局作用域,那就能解释为什么在window对象中能找到a属性了。不过,还请看看这个例子:

    javascript 复制代码
    function ja() {
        this.a = 7
    }
    let obj = {
        a: 3,
        ja: ja
    }
    obj.ja();
    console.log(window.a); // undefined
    console.log(obj.a); // 7 

    我在全局作用域中定义了一个函数ja,以及一个对象obj,对象中包含一个属性a和一个函数ja(它引用了全局作用域中的函数ja)。当我调用obj.ja()时,函数ja执行的this.a = 7赋值语句没有按照上一个例子一样将this指向全局作用域,而是指向了obj对象。所以this并未指向函数定义时所处的作用域。其实,this的指向与调用栈密不可分。

2.2、什么是调用栈?

调用栈就是从全局作用域到目标函数被调用的位置所经历的一个链路,是为了调用当前函数所执行的所有函数的一个调用次序。

javascript 复制代码
function father() {
    // 当前函数的调用栈是 全局作用域 -> father
    // 调用位置是全局作用域
    son() // 这是son的调用位置
}
​
function son() {
    // 当前调用栈是 全局作用域 -> father -> son
    // 调用位置是father
    grandson() // 这是grandson的调用位置
}
​
function grandson() {
    // 当前调用栈是  全局作用域 -> father -> son -> grandson
    // 调用位置为 son
    consloe.log('end')
}
father(); // 这是father的调用位置

找到调用栈至关重要,这样我们就能找到函数的调用位置,调用位置在调用栈的倒数第二个位置。那么,调用位置就是this指向的位置吗? 不,不,不,调用位置是我们找到this指向的基本盘,在此之上,还需要知道this的绑定规则,然后根据规则的优先级排序,最终来确认this绑定到了哪里。

三、绑定规则

3.1、默认绑定

当函数作为独立函数被调用时,也就是直接使用不带任何修饰的函数引用进行调用,将应用默认绑定。

javascript 复制代码
function defautBind() {
    console.log(this.name)
}
var name = 'tofu';
defautBind(); // tofu

在这个例子中,在全局作用域中声明了一个函数defautBind以及一个变量name,函数在全局作用域被独立调用时this.name被解析成了全局变量name。为什么? 因为函数被调用时应用了默认绑定规则,将this绑定在了全局作用域。

3.2、隐式绑定

当函数被当做对象属性进行调用时,函数会应用隐式调用。

javascript 复制代码
function implicitBind() {
    console.log(this.name)
}
var obj = {
    name: 'carrot',
    implicitBind: implicitBind
}
var name = 'tofu';
obj.implicitBind(); // carrot

在上面的例子中,我声明了一个对象obj,它拥有2个属性:nameimplicitBind,implicitBind引用了全局作用域声明的implicitBind函数。我们可以理解为obj对象中包含了implicitBind函数,当我们调用obj.implicitBind()时,implicitBind函数就有了上下文引用(obj对象),隐式调用 规则就会将implicitBind函数中的this绑定在上下文对象中。所以打印的结果是obj对象中的name而不是全局作用域中的name

3.3、显式绑定

如果像隐式绑定中的例子一样,我们每次需要将函数中的this绑定到指定的对象上时,我们都需要在指定的对象上包裹着该函数,这样是不是很麻烦,有没有办法不用包裹函数并让函数中的this指向特定对象呢?早在ES3规范出来的时候就有了callapply方法帮我们完成这个任务。ES5中的bind函数也可以做到的,只是它与call、apply有一些区别,让我们看看他们是如何工作的。

  • call、apply

    javascript 复制代码
    function displayBind(msg) {
        console.log(msg + this.name)
    }
    var obj = {
        name: 'potato'
    }
    displayBind.call(obj, 'call: '); // call: potato
    displayBind.apply(obj, ['applyt: ']); // applyt: potato
    ​
    displayBind('no bingding: ');//

    如上面例子看到的那样,call方法和apply方法会显式的将obj对象绑定在displayBind函数中的this上,这样就非常方便了。

    在这里稍作说明一下callapply这两个方法是从何而来,它们是存在于内置对象Function上的方法,是函数原型链的顶端上的方法。在javaScript很多内置对象中的函数上都可以使用它们,当然我们声明的所有自定义函数也能通过原型链使用这2个方法。所以我们可以像这个例子中一样直接使用。

  • bind

    javascript 复制代码
    function hardBind(msg) {
        console.log(msg + this.name)
    }
    var obj = {
        name: 'potato'
    }
    var hard = hardBind.bind(obj);
    hard('bind: '); // bind: potato
    • 看到区别了吗?如果没有,使用call、apply进行显式绑定的那个例子最后一行代码,我没有对displayBind函数进行显式绑定调用,也没有对象包裹的上下文调用,它采取了默认绑定规则,所以打印最终的结果为no bingding: undefined
    • 在本例中,hard函数在全局作用域中调用,它的this却绑定了obj对象。
    • 关键在var hard = hardBind.bind(obj);这一行代码,bind函数将obj对象强制绑定在了新生成的函数hard上。
    • bindcallapply函数不同,它会生成一个强制绑定某个对象的函数,这个函数无论在哪里调用,以什么方式调用,它的this都将指向它指定的对象上。

注:与call、apply方法一样,bind也是内置对象Function上的方法。

3.4、new绑定

使用new调用函数,或者说使用构造函数调用时,会生成一个新的对象,这个过程会经历几个步骤:

  1. 生成一个全新的对象,并将新对象的prototype绑定到被new调用的函数的prototype上;
  2. 将新生成的对象绑定在new调用的函数中的this上;
  3. 如果函数没有返回其他的对象,那么new函数调用将返回这个新对象。
javascript 复制代码
function newBind(name) {
    this.name = name
}
var obj = new newBind('butter');
console.log(obj.name); // butter

new绑定是最后一个this绑定方式。

四、绑定优先级

在单个绑定规则下,我们只需要知道函数的调用位置,并判断运用了哪一条绑定规则,我们就能知道this的指向。如果函数调用应用了多条规则,我们该如何判断呢,在这种情况下就需要对这4条绑定规则进行优先级排序了。现在我们来对它们进行组合测试。

在这4条绑定绑定规则中,默认绑定的优先级在其中的优先级最低,我们完全可以不用优先考虑它。

4.1、隐式绑定 VS 显式绑定

javascript 复制代码
function round_1() {
    console.log(this.name)
}
var obj1 = {
    name: 'obj1',
    round_1: round_1
}
var obj2 = {
    name: 'obj2',
    round_1: round_1
}
​
obj1.round_1(); // obj1
obj2.round_1(); // obj2
​
obj1.round_1.call(obj2); // obj2
obj2.round_1.call(obj1); // obj1
​

显而易见,显式绑定规则优先级高于隐式绑定规则。

4.2、隐式绑定 VS new绑定

javascript 复制代码
function round_2(name) {
    this.name = name
}
var obj1 = {
    round_2: round_2
}
​
obj1.round_2('tofu');
console.log(obj1.name); // tofu
​
var obj2 = new obj1.round_2('banana');
console.log(obj1.name); // tofu
console.log(obj2.name); // banana

例子中我们看到通过new调用obj1对象中的round_2函数时,并没有对obj1name进行修改,而是跨过obj对象的指向,直接创建了一个新对象obj2,并把this指向了obj2上。

所以,new绑定规则的优先级比隐式绑定规则更高。

4.3、显式绑定 VS new 绑定

由于new不能与callapply同时使用,我们只能用bindnew进行比较

javascript 复制代码
function round_3(name) {
    this.name = name
}
var obj1 = {}
​
var bound = round_3.bind(obj1);
bound('museum');
console.log(obj1.name); // museum
​
var obj2 = new bound('subway');
console.log(obj2.name); // subway
​
bound('piano')
console.log(obj1.name); // piano
console.log(obj2.name); // subway

难以置信,我看到了一场精彩的对决,第一回合,round_3函数利用bindthis强制绑定在了obj1对象上,生成新的函数bound,毫无疑问,调用bound('museum')会为obj1添加name属性并赋值为museum。第二回合,通过new调用被强制指向了thisbound函数时,new调用生成了一个新的对象obj2并将bound函数中的this指向(只是临时,或者说是调用时)新对象。两个回合下来,bind败下阵来,虽说只是一瞬间的失败,但new绑定规则确实优先级高于显式绑定规则。

需要注意的是,在new调用并非持久性的把boundthis指向obj2,我们再次调用bound('piano')时,修改的仍然是obj1对象。所以只能说使用new绑定规则在调用时优先级高于显式绑定规则。

4.4、最终优先级排序

new 绑定 > 显示绑定 > 隐式绑定 > 默认绑定

五、规则之外:箭头函数

凡是总有例外,ES6发布了一个不遵循这些规则的特殊函数:箭头函数。看看下面的例子:

javascript 复制代码
var allow = () => {
    console.log(this.name)
}
var obj1 = {
    name: 'obj',
    allow: allow
}
var obj2 = {name: 'obj2'}
var name = 'hi window';
​
obj1.allow(); // hi window ---隐式绑定规则
allow.call(obj2); // hi window ---显示绑定规则

上面的例子中,我们运用了正常函数中的2种绑定规则,它们都没生效,箭头函数中的this始终指向它声明所在的作用域,上面allow函数声明在全局,所以它的this一直指向了全局作用域。

并且它的this指向还无法更改,更不能被new调用:

javascript 复制代码
function allow() {
    return () => {
        console.log(this.name)
    }
}
var obj1 = {
    name: 'obj1'
}
var obj2 = {
    name: 'obj2'
}
​
var allowFun = allow.call(obj1)
allowFun.call(obj2); // obj1
​
var obj3 = new allowFun(); //Uncaught TypeError: allowFun is not a constructor

var allowFun = allow.call(obj1)这段代码运行的时候,也就是生成箭头函数时被显式的绑定在了obj1对象上,后面再一次显式指向到obj2对象时,失败了。new调用时也报错了。

由此可看出,箭头函数设计的初衷就是想区别于正常函数的this指向规则,它固定了箭头函数的this指向在它定义的作用域中,并且不允许更改,这为大部分无法分清正常函数this指向的同学提供了一个最简洁的解决方法,但同时回避了探究this指向的底层逻辑与规则。


总结

this并不在函数声明时或编译时被绑定,它是动态绑定的,它的指向取决于运行时的上下文,所以函数的调用位置非常重要。与此同时,还需辨别函数运用了上述4种的哪一条规则,在只应用一条规则调用函数的时候,我们可以快速的找到this绑定的对象。在应用了多个绑定规则的时候,那么我们就需要对通过4条规则的优先级(new 绑定 > 显示绑定 > 隐式绑定 > 默认绑定)进行判断。

另外,箭头函数的出现,犹如救世主一般解救了我们,我们完全没必要去弄清什么调用位置、绑定规则和规则的优先级这些底层原理。但在某种程度上说,这是在因噎废食,写正常函数的this风格代码能带给我们便利和乐趣可能使用箭头函数就很难达到了,ECMA给我们箭头函数不是为了让我们摒弃正常函数中的this机制,是为了让我们有更多的选择。

最后,本文阐述的关于this的见解只是个人当前的认知水平,如有错误的地方希望大牛们能及时指出来并反馈给我,以免误人,希望传递更准确的知识。愿所有在AI时代浪潮中的javascipt开发者们,掌握语言规则,握住AI方向盘,驶向充满无限可能的未来,谢谢!!

相关推荐
图扑软件5 分钟前
智慧城市新基建!图扑智慧路灯,点亮未来城市生活!
大数据·javascript·人工智能·智慧城市·数字孪生·可视化·智慧路灯
很萌很帅的恶魔神ww40 分钟前
HarmonyOS Next之组件之自定义弹窗(CustomDialog)
javascript
残轩40 分钟前
JavaScript/TypeScript异步任务并发实用指南
前端·javascript·typescript
AR742 分钟前
unplugin-vue-router 的基本使用
javascript
Cutey9161 小时前
前端如何实现文件上传进度条
javascript·vue.js·面试
很萌很帅的恶魔神ww1 小时前
HarmonyOS Next-元服务开发详解
javascript
前端大雄1 小时前
图片加载慢?前端性能优化中的「瘦身」秘籍大揭秘!
前端·javascript·面试
じ☆ve 清风°2 小时前
JavaScript基本知识
开发语言·javascript
鱼樱前端2 小时前
王者技能之最新Axios + TS + Element Plus 企业级二次封装(完整版)
前端·javascript·vue.js
用户2404817096212 小时前
深入了解JavaScript的Event Loop 机制
javascript