草庐IT

this指向终极解决方案(附带手写绑定函数)

chscript 2023-03-28 原文

this指向


this定义

this用于指定对当前对象的引用。


this的两种绑定方式

默认绑定

在严格模式下,全局作用域的this对象会变为undefined。在非严格模式的普通函数若不作为对象方法,它的this绑定都会自动绑定到window全局对象上,即默认绑定。

显式绑定

函数可以使用call/apply/bind等方法绑定this对象。这种方式属于强制绑定措施,this的指向是可预见的(即绑定谁就指向谁),我们重点谈谈他们的用法。

基本格式:函数名称.call(要绑定的对象, 参数列表)

call:接受一个参数列表。会立即执行。

apply:接受数组形式的参数。会立即执行。

bind:接受一个参数列表。返回原函数拷贝,不会立即执行。

若需应用,一般可以这样思考:我们想要函数的this值指向哪个对象?

let a = { name: '小红' }
function getName() {
    console.log(this.name)
}
getName() // 这里默认绑定全局对象
getName.call(a) // 显式绑定对象a

new绑定(具有显式绑定效果)

我们来看看new关键字的执行过程:

  1. 创建一个新的空对象
  2. 将构造函数的原型赋给新创建对象(实例)的隐式原型
  3. 利用显式绑定将构造函数的this绑定到新创建对象并为其添加属性
  4. 返回这个对象

很显然,这里的this的指向同样是可预见的。

基于上面的执行过程,我们可以手写实现一下(面试题):

function myNew(fn, ...args) { // 构造函数作为参数
    let obj = {}
    obj.__proto__ = fn.prototype
    fn.apply(obj, args)
    return obj
}

其中fn.apply(obj, args)对应new执行过程第3步,我们可以理解为new的过程应用了显式绑定

隐式绑定(具有显式绑定效果)

关于隐式绑定,这里提一下书里被翻译过的作者原话:

另一条需要考虑的规则是调用位置是否有上下文对象

其实隐式绑定也可以理解为它应用了显式绑定。比如我们在利用模板字面量创建对象的时候,普通函数作为对象方法拥有与显式绑定同样的效果,可以理解为已经执行了显式绑定这一过程。即函数会绑定对应的实例对象。如下:

let a = {
    x: 10,
    y: function () {    // 作为对象方法,存在显式绑定效果。
        console.log(this)
    }
}
a.y()
// 输出结果:a { x: 10 y: f } 即函数内部的this指向被绑定的实例对象

this绑定优先级

顺序:显式绑定 > 默认绑定

注意:箭头函数本身没有this,不会应用以上规则


函数的this指向

关于this指向,我们会更多的关注函数内部的this指向。一般而言会考察以下两种类型的题目:

  • 自定义对象内部函数的this指向
  • 全局对象下函数的this指向

可以利用以下准则去解决this指向问题。实际上我们只需处理这两种函数:

  1. 对于非严格模式下的普通函数会有两个情况:

    1.1 作为对象方法,this会绑定对象(执行new绑定过程)。

    1.2 不作为对象方法,在非严格模式下this默认绑定window

  2. 箭头函数没有this。它只会按规则继承最近一层普通函数全局作用域this

注意:call/apply/bind方法不能改变箭头函数的this指向,因为箭头函数本身没有this

我们尽量一次性解决所有this指向问题。首先设计这样两个结构,如下:

// 普通函数结构
function a(){}          // 普通函数声明
setTimeout(function(){})// 内置函数
(function(){})()        // 立即执行函数
return function(){}     // 匿名函数
// 箭头函数结构
let a = ()=>{}          // 箭头函数声明
setTimeout(()=>{})      // 内置函数
(()=>{})()              // 立即执行函数
return ()=>{}           // 匿名函数

利用上面的结构。分析第一种情况。如下:

let obj = {
    fun: function () { // 这里是普通函数

        console.log(this) // 普通函数作为对象方法定义。内部this指向obj

        // 以下普通函数都不作为对象方法定义,this全部指向window
        function a() { console.log(this) }; a();      // 普通函数声明执行
        setTimeout(function () { console.log(this) });// 内置函数
        (function () { console.log(this) })();        // 立即执行函数
        return function () { console.log(this) };     // 匿名函数  

    },
    arr: () => { // 这里是箭头函数

        console.log(this) // 箭头函数的this按规则继承全局作用域指向window

        // 以下普通函数都不作为对象方法定义,this全部指向window
        function a() { console.log(this) }; a();      // 普通函数声明执行
        setTimeout(function () { console.log(this) });// 内置函数
        (function () { console.log(this) })();        // 立即执行函数
        return function () { console.log(this) };     // 匿名函数  

    }
}
obj.fun()() // 会执行普通函数内部的所有普通函数和返回的匿名函数
obj.arr()() // 会执行箭头函数内部的所有普通函数和返回的匿名函数

输出结果:第一个thisobj对象,后面九个this全是window对象

接下来分析第二种情况。如下:

let obj = {
    fun: function () { // 这里是普通函数

        console.log(this) // 普通函数作为对象方法定义。this指向obj

        // 以下是箭头函数,它的this按规则继承最近一次普通函数即全部指向obj
        let a = () => { console.log(this) }; a();// 箭头函数声明
        setTimeout(() => { console.log(this) }); // 内置函数
        (() => { console.log(this) })();         // 立即执行函数
        return () => { console.log(this) };      // 匿名函数

    },
    arr: () => { // 这里是箭头函数

        console.log(this) // 箭头函数的this按规则继承全局作用域指向window

        // 以下是都是箭头函数,它的this按规则继承全局作用域全部指向window
        let a = () => { console.log(this) }; a();// 箭头函数声明
        setTimeout(() => { console.log(this) }); // 内置函数
        (() => { console.log(this) })();         // 立即执行函数
        return () => { console.log(this) };      // 匿名函数
    }
}
obj.fun()() // 会执行普通函数内部的所有箭头函数和返回的匿名函数
obj.arr()() // 会执行箭头函数内部的所有箭头函数和返回的匿名函数

输出结果:前五个this都是obj对象,后面五个this都是window对象

分析第三种情况。如下:

// 箭头函数和普通函数放在全局中声明,全部指向window
function fun() { console.log(this) }; fun();  // 普通函数声明执行
setTimeout(function () { console.log(this) });// 内置函数
(function () { console.log(this) })();        // 立即执行函数

let arr = () => { console.log(this) }; arr(); // 箭头函数声明
setTimeout(() => { console.log(this) });      // 内置函数
(() => { console.log(this) })();              // 立即执行函数

输出结果:六个this全是window对象

总结解题的关键点:首先判断是箭头函数还是普通函数。箭头函数只会按规则继承this指向。普通函数则要分两种情况:作为对象方法和不作为对象方法:作为对象方法this会绑定到对象上,不作为对象方法this则绑定到window(非严格模式)。

补充说明:普通函数作为对象方法实际上已经执行了new绑定(可以看上面的手写new过程)。因此普通函数作为对象方法,它的this会指向对象。


this绑定丢失的情况

函数别名(作为参数被传递或调用):例如obj.foo或手写Promise中的resolve方法。我们可以理解为他是一个已经定义好的函数。它的this指向具体要看他在哪里使用,而且要分清楚它是普通函数还是箭头函数。

obj.foo // 是一个函数
// 等价于下面我们定义好的普通函数或箭头,如下
let foo = function{}{ console.log(this) }
let foo = ()=>{ console.log(this) }

补充说明:实际上this丢失只有这一种情况


手写callapplybind

前置知识:ES6 剩余参数、Function.prototype原型方法定义,在调用时每个function可通过隐式原型(原型链)找到此方法。

Function.prototype.myCall = function (obj, ...args) {
    obj = obj === null || obj === undefined ? window : obj;
    return (() => {
        obj.method = this; //作为临时方法传递给对象
        obj.method(...args);
        delete obj.method;
    })();
}

callapply区别,apply的接受参数为数组形式

Function.prototype.myApply = function (obj, ...args) {
    obj = obj === null || obj === undefined ? window : obj;
    return (() => {
        obj.method = this; //作为临时方法传递给对象
        obj.method(...args[0]);
        delete obj.method;
    })();
}

普通版:bind方法是硬绑定。返回值为原函数的拷贝,其this值不可再修改。

Function.prototype.myBind = function (obj, ...args1) {
    obj = obj === null || obj === undefined ? window : obj;
    return (...args2) => {
        this.apply(obj, args1.concat(args2));
    };
}

进阶版:bind方法可支持new关键字

Function.prototype.myNewBind = function (obj, ...args1) { // 函数 1
    obj = obj === null || obj === undefined ? window : obj;
    let self = this;
    let fn = function (...args2) { // 函数 2
        return self.apply(this instanceof fn ? this : obj, args1.concat(args2));
    };
    fn.prototype = Object.create(self.prototype); // 维持其原型
    fn.prototype.constructor = fn
    return fn;
}

过程解析:

  1. 为什么要维持原形?

原生函数中bind在执行new操作时会保留其所绑定函数的原型,我们希望在执行new关键字后myNewBind函数也能拥有同样的效果。若没有进行维持原型这一步操作,我们的new操作效果其实是把返回的函数 2 作为构造函数操作去生成实例,会丢失之前所绑定函数的原型,无法实现继承。

  1. 为什么使用instanceof

判断当前对象是否为返回的构造函数所生成的实例对象。若是则认为执行了new关键字操作,返回的构造函数内部需要this代表新的实例对象,而不是旧的obj对象。

  1. 为什么要使用Object.create()

我们希望返回的函数也有自己的独立原型。直接将一个构造函数原型赋给另一个构造函数原型会使两个原型对象的数据捆绑(引用值特点)在一起,即需要保持原型对象数据的独立性。

Object.create()的运行过程手写如下:

function createObject(obj) { // 参数为原型对象
    let temp = function () { };
    temp.prototype = obj;
    return new temp();
}

参考

你不知道的JavaScript (上卷)

有关this指向终极解决方案(附带手写绑定函数)的更多相关文章

  1. ruby - 在 jRuby 中使用 'fork' 生成进程的替代方案? - 2

    在MRIRuby中我可以这样做:deftransferinternal_server=self.init_serverpid=forkdointernal_server.runend#Maketheserverprocessrunindependently.Process.detach(pid)internal_client=self.init_client#Dootherstuffwithconnectingtointernal_server...internal_client.post('somedata')ensure#KillserverProcess.kill('KILL',

  2. ruby - 在没有 sass 引擎的情况下使用 sass 颜色函数 - 2

    我想在一个没有Sass引擎的类中使用Sass颜色函数。我已经在项目中使用了sassgem,所以我认为搭载会像以下一样简单:classRectangleincludeSass::Script::FunctionsdefcolorSass::Script::Color.new([0x82,0x39,0x06])enddefrender#hamlengineexecutedwithcontextofself#sothatwithintemlateicouldcall#%stop{offset:'0%',stop:{color:lighten(color)}}endend更新:参见上面的#re

  3. ruby-on-rails - 在 ruby​​ 中使用 gsub 函数替换单词 - 2

    我正在尝试用ruby​​中的gsub函数替换字符串中的某些单词,但有时效果很好,在某些情况下会出现此错误?这种格式有什么问题吗NoMethodError(undefinedmethod`gsub!'fornil:NilClass):模型.rbclassTest"replacethisID1",WAY=>"replacethisID2andID3",DELTA=>"replacethisID4"}end另一个模型.rbclassCheck 最佳答案 啊,我找到了!gsub!是一个非常奇怪的方法。首先,它替换了字符串,所以它实际上修改了

  4. ruby - 在 Ruby 中有条件地定义函数 - 2

    我有一些代码在几个不同的位置之一运行:作为具有调试输出的命令行工具,作为不接受任何输出的更大程序的一部分,以及在Rails环境中。有时我需要根据代码的位置对代码进行细微的更改,我意识到以下样式似乎可行:print"Testingnestedfunctionsdefined\n"CLI=trueifCLIdeftest_printprint"CommandLineVersion\n"endelsedeftest_printprint"ReleaseVersion\n"endendtest_print()这导致:TestingnestedfunctionsdefinedCommandLin

  5. 屏幕录制为什么没声音?检查这2项,轻松解决 - 2

    相信很多人在录制视频的时候都会遇到各种各样的问题,比如录制的视频没有声音。屏幕录制为什么没声音?今天小编就和大家分享一下如何录制音画同步视频的具体操作方法。如果你有录制的视频没有声音,你可以试试这个方法。 一、检查是否打开电脑系统声音相信很多小伙伴在录制视频后会发现录制的视频没有声音,屏幕录制为什么没声音?如果当时没有打开音频录制,则录制好的视频是没有声音的。因此,建议在录制前进行检查。屏幕上没有声音,很可能是因为你的电脑系统的声音被禁止了。您只需打开电脑系统的声音,即可录制音频和图画同步视频。操作方法:步骤1:点击电脑屏幕右下侧的“小喇叭”图案,在上方的选项中,选择“声音”。 步骤2:在“声

  6. 【高数】用拉格朗日中值定理解决极限问题 - 2

    首先回顾一下拉格朗日定理的内容:函数f(x)是在闭区间[a,b]上连续、开区间(a,b)上可导的函数,那么至少存在一个,使得:通过这个表达式我们可以知道,f(x)是函数的主体,a和b可以看作是主体函数f(x)中所取的两个值。那么可以有,  也就意味着我们可以用来替换 这种替换可以用在求某些多项式差的极限中。方法: 外层函数f(x)是一致的,并且h(x)和g(x)是等价无穷小。此时,利用拉格朗日定理,将原式替换为 ,再进行求解,往往会省去复合函数求极限的很多麻烦。使用要注意:1.要先找到主体函数f(x),即外层函数必须相同。2.f(x)找到后,复合部分是等价无穷小。3.要满足作差的形式。如果是加

  7. ruby - 在 Ruby 中按名称传递函数 - 2

    如何在Ruby中按名称传递函数?(我使用Ruby才几个小时,所以我还在想办法。)nums=[1,2,3,4]#Thisworks,butismoreverbosethanI'dlikenums.eachdo|i|putsiend#InJS,Icouldjustdosomethinglike:#nums.forEach(console.log)#InF#,itwouldbesomethinglike:#List.iternums(printf"%A")#InRuby,IwishIcoulddosomethinglike:nums.eachputs在Ruby中能不能做到类似的简洁?我可以只

  8. ruby-on-rails - 创建 ruby​​ 数据库时惰性符号绑定(bind)失败 - 2

    我正在尝试在Rails上安装ruby​​,到目前为止一切都已安装,但是当我尝试使用rakedb:create创建数据库时,我收到一个奇怪的错误:dyld:lazysymbolbindingfailed:Symbolnotfound:_mysql_get_client_infoReferencedfrom:/Library/Ruby/Gems/1.8/gems/mysql2-0.3.11/lib/mysql2/mysql2.bundleExpectedin:flatnamespacedyld:Symbolnotfound:_mysql_get_client_infoReferencedf

  9. C51单片机——实现用独立按键控制LED亮灭(调用函数篇) - 2

    说在前面这部分我本来是合为一篇来写的,因为目的是一样的,都是通过独立按键来控制LED闪灭本质上是起到开关的作用,即调用函数和中断函数。但是写一篇太累了,我还是决定分为两篇写,这篇是调用函数篇。在本篇中你主要看到这些东西!!!1.调用函数的方法(主要讲语法和格式)2.独立按键如何控制LED亮灭3.程序中的一些细节(软件消抖等)1.调用函数的方法思路还是比较清晰地,就是通过按下按键来控制LED闪灭,即每按下一次,LED取反一次。重要的是,把按键与LED联系在一起。我打算用K1来作为开关,看了一下开发板原理图,K1连接的是单片机的P31口,当按下K1时,P31是与GND相连的,也就是说,当我按下去时

  10. 深度学习部署:Windows安装pycocotools报错解决方法 - 2

    深度学习部署:Windows安装pycocotools报错解决方法1.pycocotools库的简介2.pycocotools安装的坑3.解决办法更多Ai资讯:公主号AiCharm本系列是作者在跑一些深度学习实例时,遇到的各种各样的问题及解决办法,希望能够帮助到大家。ERROR:Commanderroredoutwithexitstatus1:'D:\Anaconda3\python.exe'-u-c'importsys,setuptools,tokenize;sys.argv[0]='"'"'C:\\Users\\46653\\AppData\\Local\\Temp\\pip-instal

随机推荐