深入理解JS中的this

1、浅谈this

1.1、调用位置

在学习this的绑定过程之前,首先要理解调用位置,即函数在代码中被调用的位置,因此我们需要分析调用栈,看以下代码

function baz(){// 当前调用栈是baz// 因此调用位置就是全局作用域console。log("baz");bar(); // <-- bar的调用位置
}
function bar(){// 当前调用栈是 baz --> bar// 因此,当前调用位置在baz中console.log("bar")foo(); // <-- foo的调用位置
}
function foo(){// 当前调用栈是 baz -> bar -> foo// 因此当前的调用位置在bar中console.log("foo")
}
baz() // <-- baz的调用位置

2、绑定规则

在函数执行过程中调用位置决定this绑定对象有四条规则

2.1、默认绑定

最常见的函数调用类型是独立函数调用,可以把这条规则看作是无法应用其他规则时的默认规则

function foo(){console.log(this.a);
}var a = 2
foo() // 2

当调用foo()时,this.a解析成了全局变量a,这是因为函数调用时应用了this的默认绑定,因此this指向全局对象

2.2、隐式绑定

我们必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用引用函数,从而把this间接(隐式)绑定到这个对象上

该绑定方式需要考虑调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含,不过这种说法可能会造成一些误导,看以下代码

function foo() {console.log(this.a);
}
var obj = {a: 2,foo: foo
}
obj.foo() // 2

无论你如何称呼这个模式,当 foo()被调用时,它的前面确实加上了对obj的引用。当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this 定到这个上下文对象。因为调用 foo()时 this 被绑定到 obj,因此 this.a和obj.a是一样的。

对象属性引用链中只有上一层或者说最后一层在调用位置中起作用,举例来说

function foo() {console.log(this.a);
}
var obj2 = {a: 42,foo: foo
}
var obj1 = {a: 2,obj2: obj2
}
obj1.obj2.foo() // 42

但是隐式绑定会出现绑定丢失的问题

2.3、显式绑定

隐式绑定需要我们在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用引用函数,从而把this间接(隐式)绑定到这个对象上,如果我们不想这样做,而想在某个对象上强制调用函数,应该怎么做?我们可以使用call(…)和apply(…)方法,这两个方法的第一个参数都是一个对象,是给this准备的,接着在调用函数时将其绑定到this,因为可以直接指定this的绑定对象,因此我们称之为显式绑定

简单的例子:

function foo(){console.log(this.a);
}
var obj = {a:2
}
foo.call(obj) // 2

通过foo.call(…),我们可以在调用foo时强制把他的this绑定到obj上

如果传入了一个原始值(字符串、数组、数字)来当做this的绑定对象,这个原始值会被转换成它的对象形式(也就是new String(…)、new Boolean(…)、new Number(…)),这通常被称为“装箱”

如果想要解决绑定丢失问题,我们可以使用显式绑定的一个变种 — 硬绑定

function foo() {console.log(this.a);}var obj = {a: 2}var bar = function () {foo.call(obj)}bar() // 2setTimeout(bar, 100) // 2// 硬绑定的bar不可能在修改它的thisbar.call(window) // 2

我们创建了函数 bar(),并在它的内部手动调用了foo.call(obj),因此强制把 foo的 this 绑定到了obj。无论之后如何调用函数bar,它总会手动在 obj上调用 foo。这种绑定是一种显式的强制绑定,因此我们称之为硬绑定

由于硬绑定是一种非常常用的模式,所以ES5提供了内置的方法Function.prototype.bind

function foo(something){console.log(this.a,something);return this.a + something
}
var obj = {a:2
}
var bar = foo.bind(obj)
var b = bar(3) // 2 3
console.log(b); // 5

bind(…)会返回一个硬编码的新函数,会把所指定的参数设置为this的上下文并调用原始函数

2.4、new绑定

在传统的面向类的语言中,“构造函数”是类中的一些特殊方法,使用new初始化类时会调用类中的构造函数。在JS中也有一个new操作符,但JS中的new的机制实际上和面向类的语言完全不同

在JS中,构造函数之所以写使用new操作符时被调用的函数。它们并不属于某个类,也不会实例化一个类,实际上,他们只是被new操作符调用的普通函数而已

使用new来调用函数,或者说发生构造函数调用时,会执行下边的操作(也是new操作符的实现原理)

  1. 创建一个全新的对象
  2. 这个新对象会执行prototype连接
  3. 这个新对象会绑定到函数调用的this
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象
function foo(a){this.a = a
}
var bar = new foo(2)
console.log(bar.a) // 2

使用new调用foo(…)时,会构建一个新对象并把它绑定到foo(…)调用中的this上。new是最后一种可以影响调用时this绑定行为的方法,称之为new绑定

3、绑定优先级

总得来说优先级如下:new > 显式绑定 > 隐式绑定 > 默认绑定

  1. 函数是否在 new 中调用 (new 绑定)? 如果是的话 this 定的是新创建的对象

    var bar = new foo()
    
  2. 函数是否通过 callapply (显式绑定)或者硬绑定调用?如果是的话,this 绑定的是指定的对象。

    var bar = foo.call(obj2)
    
  3. 函数是否在某个上下文对象中调用 (隐式绑定)?如果是的话,this 定的是那个上下文对象。

    var bar = obj1.foo()
    
  4. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到全局对象。

    var bar = foo()
    

大部分就是上述四种情况所述,但是凡事都有例外

4、绑定例外

在某些情况下this的绑定会出乎意料,当我们认为应当使用前三种规则时,但实际上使用的可能是默认绑定规则

4.1、被忽略的this

如果我们把null或者underfined作为this的绑定对象传入call、apply或bind,这些值在调用时会被忽略,实际上应用的是默认绑定规则

function foo() {console.log(this.a);
}
var a = 2
foo.call(null)//2

那么在什么情况下我们会传入null呢?

一种常见的做法是使用apply(…)来“展开”一个数组,并当作参数传人一个函数。类似地,bind(…)可以对参数进行柯里化 (预先设置一些参数),这种方法有时非常有用

function foo(a, b) {console.log("a:" + a + ", b:" + b);
}
// 把数组“展开”成参数
foo.apply(null, [2, 3]); // a:2, b:3
// 使用 bind(..)进行柯里化
var bar = foo.bind(null, 2); 
bar(3); // a:2,b:3

这两种方法都需要传人一个参数当作 this 的绑定对象。如果函数并不关心 this 的话,你仍然需要传人一个占位值,这时 null 可能是一个不错的选择,就像代码所示的那样

然而,总是使用null来忽略 this 绑定可能产生一些副作用。如果某个函数确实使用了this (比如第三方库中的一个函数),那默认绑定规则会把 this 定到全局对象 (在浏览器中这个对象是 window),这将导致不可预计的后果 (比如修改全局对象)。

一种“更安全”的做法是传入一个特殊的对象,把 this 绑定到这个对象不会对你的程序产生任何副作用。就像网络(以及军队)一样,我们可以创建一个“DMZ”(demilitarizedzone,非军事区)对象它就是一个空的非委托的对象
如果我们在忽略 this 绑定时总是传入一个 DMZ 对象,那就什么都不用担心了,因为任何对于 this 的使用都会被限制在这个空对象中,不会对全局对象产生任何影响。
在 JavaScript 中创建一个空对象最简单的方法都是 object.create(null)。object.create(null)和{}很像,但是并不会创建 object.prototype 这个委托,所以它比{}“更空”:

function foo(a, b) {console.log("a:" + a + ", b:" + b);
}
// 我们的 DMZ 空对象
var ø = Object.create(null);
// 把数组展开成参数
foo.apply(ø, [2, 3]); // a:2, b:3
// 使用 bind(..) 进行柯里化
var bar = foo.bind(ø, 2);
bar(3); // a:2, b:3

使用变量名 ø 不仅让函数变得更加“安全”,而且可以提高代码的可读性,因为 ø 表示“我希望 this 是空”,这比 null 的含义更清楚。

4.2、间接引用

另一个需要注意的是,你有可能(有意或者无意地)创建一个函数的“间接引用”,在这种情况下,调用这个函数会应用默认绑定规则。

间接引用最容易在赋值时发生:

function foo() {console.log(this.a);
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); // 3
(p.foo = o.foo)(); // 2

赋值表达式 p.foo = o.foo 的返回值是目标函数的引用,因此调用位置是 foo() 而不是p.foo() 或者 o.foo()。根据我们之前说过的,这里会应用默认绑定。
注意:对于默认绑定来说,决定 this 绑定对象的并不是调用位置是否处于严格模式,而是函数体是否处于严格模式。如果函数体处于严格模式,this 会被绑定到 undefined,否则this 会被绑定到全局对象。

4.3、软绑定

之前我们已经看到过,硬绑定这种方式可以把 this 强制绑定到指定的对象(除了使用 new时),防止函数调用应用默认绑定规则。问题在于,硬绑定会大大降低函数的灵活性,使用硬绑定之后就无法使用隐式绑定或者显式绑定来修改 this。
如果可以给默认绑定指定一个全局对象和 undefined 以外的值,那就可以实现和硬绑定相同的效果,同时保留隐式绑定或者显式绑定修改 this 的能力。可以通过一种被称为软绑定的方法来实现我们想要的效果:

if (!Function.prototype.softBind) {Function.prototype.softBind = function (obj) {var fn = this;// 捕获所有 curried 参数var curried = [].slice.call(arguments, 1);var bound = function () {return fn.apply((!this || this === (window || global)) ?obj : this,curried.concat.apply(curried, arguments));};bound.prototype = Object.create(fn.prototype);return bound;};
}

除了软绑定之外,softBind(…) 的其他原理和 ES5 内置的 bind(…) 类似。它会对指定的函数进行封装,首先检查调用时的 this,如果 this 绑定到全局对象或者 undefined,那就把指定的默认对象 obj 绑定到 this,否则不会修改 this。此外,这段代码还支持可选的柯里化

4.4、箭头函数中的this

箭头函数不使用 this 的四种标准规则,而是根据外层(函数或者全局)作用域来决定 this。

function foo() {// 返回一个箭头函数return (a) => {//this 继承自 foo()console.log(this.a);};
}
var obj1 = {a: 2
};
var obj2 = {a: 3
};
var bar = foo.call(obj1);
bar.call(obj2); // 2, 不是 3 !

foo() 内部创建的箭头函数会捕获调用时 foo() 的 this。由于 foo() 的 this 绑定到 obj1,bar(引用箭头函数)的 this 也会绑定到 obj1,箭头函数的绑定无法被修改。(new 也不行!)

箭头函数可以像 bind(…) 一样确保函数的 this 被绑定到指定对象,此外,其重要性还体现在它用更常见的词法作用域取代了传统的 this 机制。实际上,在 ES6 之前我们就已经在使用一种几乎和箭头函数完全一样的模式。

function foo() {var self = this; // lexical capture of thissetTimeout(function () {console.log(self.a);}, 100);
}
var obj = {a: 2
};
foo.call(obj); // 2

虽然 self = this 和箭头函数看起来都可以取代 bind(…),但是从本质上来说,它们想替代的是 this 机制。

5、总结

如果要判断一个运行中函数的 this 绑定,就需要找到这个函数的直接调用位置。找到之后就可以顺序应用下面这四条规则来判断 this 的绑定对象。

  1. 由 new 调用?绑定到新创建的对象。
  2. 由 call 或者 apply(或者 bind)调用?绑定到指定的对象。
  3. 由上下文对象调用?绑定到那个上下文对象。
  4. 默认:在严格模式下绑定到 undefined,否则绑定到全局对象。

一定要注意,有些调用可能在无意中使用默认绑定规则。如果想“更安全”地忽略 this 绑定,你可以使用一个 DMZ 对象,比如 ø = Object.create(null),以保护全局对象。ES6 中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定this,具体来说,箭头函数会继承外层函数调用的 this 绑定(无论 this 绑定到什么)。这其实和 ES6 之前代码中的 self = this 机制一样。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/131911.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

财务RPA机器人如何使用

随着科技的不断发展&#xff0c;自动化技术在各个领域得到了广泛应用&#xff0c;在财务领域&#xff0c;RPA机器人已经成为一种新兴的技术手段&#xff0c;帮助众多企业实现了财务流程的自动化&#xff0c;大大提高了工作效率&#xff0c;降低人力成本。 本文将详细介绍财务RP…

AtCoder Beginner Contest 327 G. Many Good Tuple Problems(带标号二分图计数+有区别小球放入有区别盒子)

题目 一个长为n(n<30)的原始序列x&#xff0c;x[i]可以取值0或1 一个长为m(m<1e9)的点对序列(s,t)&#xff0c; s序列第i项和t的第i项&#xff0c;均可以取值[1,n]&#xff0c; 如果构造好s和t后&#xff0c;对任意都存在01序列x使得&#xff0c; 则称这个序列是合法…

【RabbitMQ】 RabbitMQ 消息的延迟 —— 深入探索 RabbitMQ 的死信交换机,消息的 TTL 以及延迟队列

文章目录 一、死信交换机1.1 什么是死信和死信交换机1.2 死信交换机和死信队列的创建方式 二、消息的 TTL2.1 什么是消息的 TTL2.2 基于死信交换机和 TTL 实现消息的延迟 三、基于 DelayExchang 插件实现延迟队列3.1 安装 DelayExchang 插件3.2 DelayExchang 实现消息延迟的原理…

MySQL5.7小版本升级-In-Place方式

在 Centos7.9上升级二进制方式安装的MySQL 。升级方式有就地升级和逻辑升级2种方法&#xff0c;本实验演示In-Place Upgrade就地升级的方式将mysql-5.7.17升级至5.7.44 In-Place Upgrade就地升级Logical Upgrade逻辑升级 升级方式介绍 In-Place Upgrade就地升级 In-Place U…

perl列表创建、追加、删除

简介 perl 列表追加元素 主要是通过push和unshift函数来实现。其中&#xff0c;push是追加到列表尾&#xff0c;unshift是追加到列表头。 perl列表删除元素 主要是通过pop和shift函数来实现。其中&#xff0c;pop是从列表尾删除一个元素&#xff0c; shift是从列表头删除一…

6大场景,玩转ChatGPT!

文章目录 一、故事叙述提问举例 二、产品描述提问举例 三、报告撰写提问举例 四、邮件和信件撰写提问举例 五、新间稿和公告撰写提问举例 六、学术论文和专业文章撰写提问举例 本文是在GPT3.5版本下演示的 我们知道AI技术不仅能够自动生成文章和内容&#xff0c;还可以根据我们…

【大数据】NiFi 中的重要术语

NiFi 中的重要术语 1.Flow Controller2.Processor3.Connection4.Controller Service5.Process Group6.FlowFile 那些一个个黑匣子称为 Processor&#xff0c;它们通过称为 Connection 的队列交换名为 FlowFile 的信息块。最后&#xff0c;FlowFile Controller 负责管理这些组件…

软考高级系统架构设计师系列之:软考高级系统架构设计师论文专题

软考高级系统架构设计师系列之:软考高级系统架构设计师论文专题 一、论文相关内容二、论文专题大纲三、论文考试方式四、历年真题汇总分析五、论文常见问题六、论文评分标准七、搭建论文万能模版八、论文万能模版公式九、搭建论文万能模版—摘要十、搭建论文万能模版—背景十一…

XSAN数据恢复-存储空间架构迁移时误格式化存储系统的XSAN数据恢复案例

XSAN数据恢复环境&#xff1a; 昆腾存储&#xff0c;MAC OS操作系统&#xff0c;存放视频类数据&#xff08;MXF、MOV等格式文件&#xff09;。 XSAN故障&检测&#xff1a; 将存储空间从XSAN架构迁移到STORNEXT架构后&#xff0c;存储空间中数据全部丢失。 故障存储中一共…

Oracle常用运维SQL-SQL执行性能及锁表等查询分析

oracle相关系列文章: docker–在Anaconda jupyter 容器中使用oracle数据源时,Oracle客户端安装配置及使用示例 Oracle常用运维SQL–用户管理、数据导入、导出的实用脚本 Oracle TEMPORARY TABLE 临时表的使用及删除报ORA-14452错误的解决办法 Oracle常用运维SQL-SQL执行性能及…

R 复习 菜鸟教程

R语言老师说R好就业&#xff0c;学就完了 基础语法 cat()可以拼接函数&#xff1a; > cat(1, "加", 1, "等于", 2, \n) 1 加 1 等于 2sink()&#xff1a;重定向 sink("r_test.txt", splitTRUE) # 控制台同样输出 for (i in 1:5) print(i…

蓝桥杯官网填空题(方格计数)

题目描述 本题为填空题&#xff0c;只需要算出结果后&#xff0c;在代码中使用输出语句将所填结果输出即可。 如下图所示&#xff0c;在二维平面上有无数个 11 的小方格。 我们以某个小方格的一个顶点为圆心画一个半径为 50000 的圆。 你能计算出这个圆里有多少个完整的小方…

【星海出品】VUE(五)

表单 表单输入绑定 只需要v-model声明一下这个变量就可以。 还可以选择不同的类型&#xff0c;例如 type"checkbox“ v-model 也提供了 lazy、number、.trim 功能&#xff0c;只需要在v-model后面加入.lazy 例如&#xff1a;v-model.lazy”message“ <template><…

Azure 机器学习 - 设置 AutoML 训练时序预测模型

目录 一、环境准备二、训练和验证数据三、配置试验支持的模型配置设置特征化步骤自定义特征化 四、可选配置频率和目标数据聚合启用深度学习目标滚动窗口聚合短时序处理非稳定时序检测和处理 五、运行试验六、用最佳模型进行预测用滚动预测评估模型精度预测未来 七、大规模预测…

Flink源码解析八之任务调度和负载均衡

源码概览 jobmanager scheduler:这部分与 Flink 的任务调度有关。 CoLocationConstraint:这是一个约束类,用于确保某些算子的不同子任务在同一个 TaskManager 上运行。这通常用于状态共享或算子链的情况。CoLocationGroup & CoLocationGroupImpl:这些与 CoLocationCon…

css主题切换

html <!DOCTYPE html> <html> <head><title>主题切换示例</title> </head> <body><div class"app"><button id"themeButton">切换主题</button><div class"content"><h1…

已完结,给小白的《50讲Python自动化办公》

大家好&#xff0c;这里是程序员晚枫&#xff0c;小红薯也叫这个名。 写在前面 上个周末去成都参加了第8届中国开源年会&#xff0c;认识了很多行业前辈和优秀的同龄人。 我发现在工作之外还能有一番事业的人&#xff0c;都有一个让我羡慕的共同点&#xff1a;有一个拿得出手…

【Go】-调用企微机器人

没有什么好讲的&#xff0c;直接贴代码&#xff0c;消息格式我用的markdown。 sendMsg.go&#xff1a; package mainimport ("flag""fmt""github.com/goccy/go-json""github.com/tidwall/gjson""io/ioutil""net/http…

C++笔记之表驱动法

C笔记之表驱动法 code review! 文章目录 C笔记之表驱动法0.数组小技巧1.std::map实现2.结构体实现3.数组和结构体结合实现表驱动法-存储函数指针4.表驱动法概念-ChatGPT5. 直接访问表&#xff08;Direct Access Table&#xff09;的示例6. 索引访问表&#xff08;Indexed Acc…

Yolov8目标识别与实例分割——算法原理详细解析

前言 YOLO是一种基于图像全局信息进行预测并且它是一种端到端的目标检测系统&#xff0c;最初的YOLO模型由Joseph Redmon和Ali Farhadi于2015年提出&#xff0c;并随后进行了多次改进和迭代&#xff0c;产生了一系列不同版本的YOLO模型&#xff0c;如YOLOv2、YOLOv3、YOLOv4&a…