当前位置:首页 > 文章 > 正文内容

你小子,又在偷偷学this指向

廖万里1年前 (2022-11-06)文章72198

你小子,又在偷偷学this指向


深入了解V8执行流程,执行上下文和范围!如中所述,每当JavaScript执行一段可执行代码时,它都会创建一个相对于。每个执行上下文包含三个重要的属性:

词汇环境成分;

可变环境组件;

初始化此的值;

如果你对作用域和执行上下文不太了解,可以看一下上面的文章,里面讲述了V8的编译过程,以及作用域和执行上下文等一些比较难懂的概念。相信你看完会收获很多!

这是什么

与其他语言相比,JavaScript中函数的这个关键字的表达方式略有不同。另外,严格模式和非严格模式也有一些区别。在全局上下文中,它指向顶层对象(浏览器中的窗口),无论是否处于严格模式。

在大多数情况下,调用函数的方式决定了这个(运行时绑定)的值。这不能在执行过程中赋值,并且每次调用函数时其值可能不同。

这是在运行时绑定的,而不是在编写时绑定的,它的执行上下文取决于调用函数时的各种条件。

this的绑定与函数声明的位置无关,只取决于如何调用函数。

约束规则

默认绑定

首先要介绍的是最常用的一类函数调用:独立函数调用。想想下面的代码:

函数f00() {

console . log(this . a);

}

var a = 2;

foo();// 2

复制代码

在开头提到的文章中,说的是在全局范围内用var关键字声明的变量和在全局范围内声明的函数会被挂载到全局对象(window)上。

当我们调用foo()时,我们都知道全局声明的函数的作用域是浏览器中的顶级globalObject,也就是window。

通过观察我们可以看到,在代码中,foo()是用函数引用直接调用的,没有任何修饰,所以只能使用默认绑定。所以函数中的这个是window,也就是window.a,所以自然输出2。

如果使用严格模式,全局对象将不会用于默认绑定,因为这将被绑定到undefined

函数f00() {

“使用严格”;

console . log(this . a);

}

var a = 2;

f00();//无法读取未定义的属性(读取“a”)

//因为严格默认情况下,这是默认绑定为undefined的,所以this.a等于undivided.a

//因为undefined下没有A的属性,所以会报错类型。

复制代码

值得注意的是,如果foo()运行在非严格模式下,默认绑定可以绑定到全局对象,但严格模式下的foo()不影响默认绑定。

函数f00() {

console . log(this . a);

}

var a = 2;

(函数(){

“使用严格”;

f00();// 2

})();

复制代码

隐式结合

隐式绑定的规则是在调用位置是否有一个上下文对象,或者它是否被一个对象拥有或包含。但是,也不一定能这么说。首先,考虑下面的代码:

函数foo() {

console . log(this . a);

}

var obj = {

甲:111,

foo,

};

obj . foo();// 111

复制代码

首先要注意的是foo()是如何声明的,然后如何作为引用属性添加到abj对象中。但无论是直接在obj中定义,还是先定义后作为引用属性添加,这个函数严格来说都不是obj对象。

但是,调用位置会使用obj上下文来引用函数,所以在调用函数时,可以说obj对象“拥有”或“包含”了函数引用。

当一个函数引用一个上下文对象时,隐式绑定规则将在对这个上下文对象的函数调用中绑定它。所以this.a和obj.a是一样的。

例如,只有对象引用链的上层或最后一层在调用位置起作用

函数foo() {

console . log(this . a);

}

var obj2 = {

甲:111,

foo,

};

var obj1 = {

答:777,

obj2,

};

obj 1 . obj 2 . foo();// 111

//对象obj2是最后一层

// obj1.obj2仅用于属性查找,尚未调用。

复制代码

脱离原始上下文的函数

最常见的绑定问题之一是隐式绑定的函数将丢失绑定对象,这意味着它将应用默认的绑定默认值。

函数foo() {

console . log(this . a);

}

var obj = {

答:2,

foo,

};

var bar = obj.foo//函数别名

Var = "我是窗下";

bar();//我是一个窗下的人

复制代码

bar虽然是对obj.foo的引用,但实际上是引用foo函数本身,所以此时的bar()实际上是一个普通的函数调用,应用了默认绑定。

这实际上重新定义了一个bar函数,它和对象的结构一样,是重新赋值的。请参考代码:

函数foo() {

console . log(this . a);

}

var obj = {

答:2,

foo,

};

var { foo } = obj//这相当于重新定义一个函数或者它是一个函数别名。

Var = "我是窗下";

foo();//我是一个窗下的人

var object = {

时刻:777,

年龄:18,

};

console.log(对象);//{时刻:777,年龄:18}

var { moment } = object

Moment = "牛逼";

console.log(时刻);//牛逼

console.log(对象);//{时刻:777,年龄:18}

复制代码

上面的代码,解构后的变量moment,实际上是在全局范围内创建了一个变量moment,赋给777。后面对变量的直接修改并不会修改object对象中的属性矩。

作为参数的功能

函数foo() {

console . log(this . a);

}

功能栏(fn) {

// fn其实指的是foo。

fn();

}

var obj = {

答:777,

foo,

};

Var = "牛逼,那也行";

bar(obj . foo);//牛逼,这个就行。

复制代码

传递参数实际上是一种隐式赋值,所以我们在传入函数的时候也会被隐式赋值。上面的代码实际上是下面代码的变体:

函数foo() {

console . log(this . a);

}

功能栏(){

const fn = obj.foo

fn();

}

var obj = {

答:777,

foo,

};

Var = "牛逼,那也行";

bar();//牛逼,这个就行。

复制代码

显示绑定

在JavaScript中,您可以使用调用(...)并申请(...)方法,无论是宿主环境提供的一些函数,还是自己创建的函数。

它们的第一个参数是一个对象,是为此准备的,然后在调用函数时绑定到这个。因为可以直接指定这个的绑定对象,我们称之为显示绑定,

这里就不说apply和call的语法规则了。如有需要,可以咨询mdn官网。

硬装订

硬绑定可以将此强制绑定到指定的对象(new除外)。既然有硬绑定,自然就有软绑定,后面会讲到。

函数foo() {

console . log(this . a);

}

var obj = {

答:2,

};

var bar = function () {

foo . call(obj);

};

bar();// 2

setTimeout(巴,1000);// 2

//硬绑定栏不能再修改他的这个

bar.call(窗口);// 2

复制代码

应用方法具有相同的结果,但是参数的方式不同。

bind方法将返回一个硬编码的新函数,该函数将您指定的参数设置为这个上下文调用的原始参数。

API调用的“上下文”

JavaScript语言和宿主环境提供了许多内置函数,它们都提供了一个可选的参数,这个参数通常会成为一个上下文。其功能与bind(...).确保您的回调函数使用指定的this。

函数回调(元素){

console.log(element,this . id);

}

var obj = {

Id:“真好”,

};

//当foo(...)叫做

[1, 2, 3].forEach(回调,obj);

// 1 '真好看' 2 '真好看' 3 '真好看'

//我的地图也一样。

[1, 2, 3].map(回调,obj);

// 1 '真好看' 2 '真好看' 3 '真好看'

复制代码

新绑定

在我们开始讨论绑定之前,我想你已经知道当使用new调用构造函数时会发生什么。我们再来回顾一下:

在内存中创建一个新对象;

这个新对象内部的[[prototype]]属性被赋值为构造函数的prototype属性(不知道这个也可以点这里);

这个在构造函数里面被赋给这个新对象(也就是这个指向新对象);

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

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

函数Foo(力矩){

this.moment = moment

}

var bar = new Foo(777);

console . log(bar . a);// 777

复制代码

当使用new调用Foo(...),我们将构造一个新的对象,并在Foo中将其绑定到这个(...)打电话。

让我们考虑一下代码输出是什么:

var mayDay = {

瞬间:“瞬间”,

};

函数Foo() {

this.moment = 777

返回五月天;

}

var bar = new Foo();

console . log(bar . moment);

复制代码

输出的最终结果是moment,也就是这个绑定了五月天对象,那么为什么会这样呢?

答案在new的最后一个程序里“如果构造函数返回非空对象,就返回对象;否则,返回到新创建的“新对象”规则。

换句话说,如果构造函数返回一个对象,该对象将作为整个表达式的值返回,而传递的构造函数的这个将被丢弃。

如果构造函数返回非对象类型,则返回值被忽略,并返回新创建的对象。

var mayDay = {

瞬间:“瞬间”,

};

函数Foo() {

this.moment = 777

返回111;//这里的返回值已经更改

}

var bar = new Foo();

console . log(bar . moment);// 777输出的是新对象的矩。

复制代码

类上下文

这个类中的表现和函数中的表现类似,因为类本质上也是函数,但是有一些区别和考虑。在类的构造函数中,这是一个常规对象。类中的所有非静态方法都将添加到此的原型中:

课程示例{

构造函数(){

const proto = object . getprototypeof(this);

console . log(object . getownpropertymanames(proto));

}

first() {}

second() {}

Static third() {} //这不是在this上,而是在类本身上

}

新示例();// ['构造函数','第一个','第二个']

复制代码

箭头函数调用

箭头表达式的语法比函数表达式更简洁,它没有自己的this、arguments、super或new.target箭头表达式更适合那些需要匿名函数的地方,它不能作为构造函数使用。因为arrow函数不具备这一点,所以不能自然使用new运算符。

var moment = " moment

var bar = {

时刻:777,

常规:函数(){

console . log(this . moment);

},

箭头:()=> {

console . log(this . moment);

},

nest: function () {

var回调= () => {

console . log(this . moment);

};

回调();

},

};

bar . general();// 777

bar . arrow();//时刻

bar . nest();// 777

复制代码

第一个常见的功能是隐式绑定,我们前面提到过。

第二个调用,因为arrow函数没有自己的this,所以会在arrow函数的上一级寻找普通函数的this,然后就变成默认绑定,这是一个全局调用。

第三个和第二个差不多,但是它找的上层是函数嵌套,是隐式绑定,自然在对象内部输出monent。

虽然arrow函数不能通过call、applu、bind来绑定这个,但是它可以在cache arrow函数的上层绑定这个普通的函数,例如:

var foo = {

时刻:777,

常规:函数(){

console . log(this . moment);

return () => {

console.log("箭头:",this . moment);

};

},

};

var obj = {

瞬间:“瞬间”,

};

foo.general()。call(obj);// 777 "箭头:777 "

foo . general . call(obj)();//'时刻' '箭头:' ' '时刻'

复制代码

注意,这在settimeout和自执行函数中指向窗口。

setTimeout(函数foo() {

console . log(this);//窗口

}, 0);

(函数(){

console . log(this);//窗口

})();

复制代码

因为settimeout方法是挂载在window对象上的,所以在执行settimeout的时候,这个在执行回调中指向调用settimeout的对象,所以是window。

优先

如果可以对某个呼叫位置应用多个规则会怎么样?为了解决这个问题,必须优先考虑这些规则。显然,默认绑定的优先级是四个规则中最低的。

函数foo() {

console . log(this . a);

}

var obj1 = {

甲:666,

foo,

};

var obj2 = {

答:777,

foo,

};

obj 1 . foo();// 666

obj 2 . foo();// 777

obj 1 . foo . call(obj 2);// 777

obj 2 . foo . call(obj 1);// 666

复制代码

从上面的代码可以看出,显式绑定的优先级比隐式绑定高,也就是说,首先要考虑显式绑定能不能存在。

函数foo(年龄){

this.age =年龄;

}

var obj1 = {

foo,

};

var obj 2 = { };

obj 1 . foo(2);

console . log(obj 1 . age);// 2

obj1.foo.call(obj2,3);

console . log(obj 2 . age);// 3

var bar = new obj 1 . foo(7);

console . log(obj 1 . age);// 2

console . log(bar . age);// 7

复制代码

可以看出,新绑定的优先级高于隐式绑定,但是新绑定和显示绑定谁的优先级更高呢?

因为new和call/apply不能一起用,所以不能用new foo.call直接测试(...),但是我们可以用硬绑定来测试它们的优先级。

函数foo(年龄){

this.age =年龄;

}

var obj 1 = { };

var bar = foo . bind(obj 1);

酒吧(2);

console . log(obj 1 . age);// 2

var baz =新酒吧(3);

console . log(obj 1 . age);// 2

console . log(baz . age);// 3

复制代码

出乎意料的是,bar被绑定到obj1,但是new bar(3)并没有像我们语句中那样把obj1.age改为3。相反,new修改了硬绑定(到obj1)以在bar(...).

这是因为调用new时,bind之后的函数会忽略bind的第一个参数。稍后,我们将使用bind方法的ployfill实现来解释为什么会发生这种情况。

综上所述,他们的优先顺序是:

新电话;

调用、应用、绑定调用;

隐式绑定(对象方法调用);

默认绑定(普通函数调用);

绑定的Ployfill实现

function . prototype . bind = function(指针){

如果(键入此!== "函数"){

抛出新类型错误(

" Function.prototype.bind -试图绑定的内容是不可调用的"

);

}

//将参数转换为数组

const args = array . prototype . slice . call(arguments,1);

const self = this

const new func = function(){ };

const fBound = function () {

返回self.apply(

//如果是新的运算符,重新绑定这个

NewFunc && pointer的这个实例?这个:指针,

args . concat(array . prototype . slice . call(参数))

);

};

new func . prototype = this . prototype;

fbound . prototype = new new func();

返回fBound

};

复制代码

其中,以下是与新修改本相关的代码:

NewFunc && pointer的这个实例?这个:指针;

//...和;

new func . prototype = this . prototype;

fbound . prototype = new new func();

复制代码

软装订

正如我们前面提到的,这种硬绑定方法可以强制将其绑定到指定的对象(除非使用new),并防止函数调用应用默认的绑定规则。

但问题是硬绑定会大大降低函数的灵活性。在使用硬绑定之后,你不能使用隐式绑定或显式绑定来修改这个的能力。具体来说,请参见实现:

function . prototype . softbind = function(object){

设fn = this

//捕获所有定制的参数

const curried =[]. slice . call(arguments,1);

const bound = function () {

返回(

fn.apply(!this || this ===(窗口||全局)?对象:这个),

curried.concat.apply(curried,参数)

);

};

bound . prototype = object . create(fn . prototype);

返回绑定;

};

函数foo() {

console . log(this . name);

}

const obj = {

名称:“obj”,

};

const obj2 = {

名称:“obj2”,

};

const obj3 = {

名称:“obj3”,

};

const foo obj = foo . softbind(obj);

foo obj();// obj

obj 2 . foo = foo . softbind(obj);

obj 2 . foo();// obj2

foo obj . call(obj 3);// obj3

setTimeout(obj2.foo,1000);// obj

复制代码

如您所见,foo()的软绑定版本可以手动将其绑定到不同的对象。

参考文章

不知道JavaScript的书都卷起来了

MDN

结局

一点点this指向涵盖了new、call、apply、bind、arrow函数等的用法。从而延伸到范围、闭包、原型链、继承、严格模式,这个实力不可小觑。


本文链接:https://www.kkkliao.cn/?id=242 转载需授权!

分享到:

添加博主微信共同交流探讨信息差网赚项目: 19528888767 , 请猛戳这里→点我添加

版权声明:本文由廖万里的博客发布,如需转载请注明出处。

“你小子,又在偷偷学this指向” 的相关文章

双11想买台便宜的512GB手机,真的就这么难吗?

双11想买台便宜的512GB手机,真的就这么难吗?

双十一可能是很多小伙伴换手机的时间,但是换手机的时候却面临了一个问题,现在手机基本都是128GB起步,但是很多人买手机又想买大内存版本,而大版本的又有些贵,这样就导致本来预算是旗舰机,结果只能换中端机,其实大可不必,因为有这么几款512GB的大内存手机,价格不贵,而且性能也很强,一起看看吧。第一款:...

六零后已经渐渐老了,都是独生子女家庭,以后怎样养老?

六零后已经渐渐老了,都是独生子女家庭,以后怎样养老?

我是68年的,今年54岁,我也只有一个女儿,以后怎么养老的问题,我在十年前就开始准备了,所以我的养老问题我现在一点都不担心,因为我早已经准备好了。我从上班到退休一直在银行储蓄柜工作。工作小柜台,人生大舞台,在几十年的工作中我见识了形形色色的人,也见过很多老人特别是没有退休金的老人晚年凄苦的生活。所以...

你最讨厌QQ什么?

你最讨厌QQ什么?

作为中国最早的社交软件之一, QQ承载着无数的青春。QQ最初诞生的时候,是为了方便人们之间的交流而诞生的。那时候 QQ作为我们联系的主要工具,人们之间可以进行即时通讯。我们每天都会在 QQ上和不同的人进行沟通互动。聊天的内容也十分的丰富,有的时候聊天的内容甚至超过了现在人们生活的内容。而其中最让人讨...

最简单的生活一天花多少钱?

最简单的生活一天花多少钱?

90后负债女孩的极简主义:月薪6000+,一天的真实花销精简但你绝对想不到!广西农村姑娘在广州,网贷负债6w,人情债接近3w,到手月薪6000+,在珠江新城商业CBD上班,一天真实的花销在多少?道出多少负债人的辛酸!疫情这3年的收入,固定死工资基本就这个数了,负债之下一直没啥存款,都是还没发工资都被...

如何让自己的努力更有效率?

如何让自己的努力更有效率?

收到了某个朋友发来的困惑咨询,抽象出来后整理出如下问题:为何自己很努力但觉得没有成长,做了很多事情却感觉没有核心竞争力,有浑身的精力不知道该往何处发力,应该如何破局?我是一名技术型产品经理,已经工作了3年,但是感觉自己陷入了成长迷茫期。 团队很重视技术,我花了很多时间来弥补技术知识,但是发现干不过研...

六年前端面试报告

六年前端面试报告

2022.10.20 在当前公司待了两年多,被离职了,拿了点赔偿金继续面试。薪资期望 13-15, 趁着今天1024整理下面试过程。上一次面试我是4年经验,简历也好改,加上两年经验,补上现公司项目就出去找工作了。简历改完后,首先分析下自己现阶段水平,大概能要多少,定一个期望薪资。再就是背面试题了。自...