前言

  • 在 深入理解V8执行流程、执行上下文和作用域! 中讲到,JavaScript每执行一段可执行代码时,都会创建相对于的执行上下文,对于每个执行上下文,都包含三个重要的属性:
  1. 词法环境 (LexicalEnvironment) 组件;
  2. 变量环境 (VariableEnvironment) 组件;
  3. 初始化 this 的值;

如果对作用域和执行上下文不太了解的同学可以看一下上面的提到的文章,这里讲述了 V8 的编译过程,以及作用域和执行上下文等令人难懂的概念,相信你阅读完会有很大的收获!

什么是this

  • 与其他语言相比,函数的 this 关键字在 JavaScript 中的表现略有不同,此外,在严格模式和非严格模式之间也会有一些差别。在全局上下文中,无论是严格模式或者非严格模式,this都指向顶层对象(浏览器中是window)。
  • 在绝大多数情况下,函数的调用方式决定了 this 的值(运行时绑定)。this 不能在执行期间被赋值,并且在每次函数被调用时 this 的值也可能会不同。
  • this是在运行时绑定的的,并不是在编写时绑定的,它的执行上下文取决于函数调用时的各种条件。
  • this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。

绑定规则

默认绑定

  • 首先要介绍的是最常用的函数调用类型: 独立函数调用,思考以下代码:
function f00() {console.log(this.a);
}var a = 2;foo(); // 2
复制代码
  • 在开头提到的那篇文章中说过,在全局作用域下用 var 关键字声明的变量和在全局声明的 函数 会被挂载到全局对象(window)上。
  • 当我们看到调用 foo() 时,我们都知道,全局声明的函数的作用域是顶层的 globalObject 在浏览器中也就是 window 。
  • 通过观察,我们可以看出,在代码中,foo() 是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,所以函数中的 this 为 window,也就是 window.a,所以自然而然的就输出 2 了。
  • 如果使用严格模式 strict mode ,则不会将全局对象用于默认绑定,因为 this 会绑定到 undefined;
function f00() {"use strict";console.log(this.a);
}var a = 2;f00(); // Cannot read properties of undefined (reading 'a')// 因为严格默认情况下,默认绑定,this会被绑定为 undefined ,所以this.a也就等于undivided.a
// 因为 undefined 下没有 a 的属性,所以会报类型错误
复制代码
  • 值得注意的是,如果 foo()运行在非 strict mode 下时,默认绑定才能绑定到全局对象,在严格模式 foo() 则不影响默认绑定。
function f00() {console.log(this.a);
}var a = 2;(function () {"use strict";f00(); // 2
})();
复制代码

隐式绑定

  • 隐式绑定的规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含,但是这样的说法可能不太会,先来思考下面的代码:
function foo() {console.log(this.a);
}var obj = {a: 111,foo,
};obj.foo(); // 111
复制代码
  • 首先需要注意的是 foo() 的声明方式,以其之后是如何被当做引用属性添加到 obj 对象中的。但是无论是直接在 obj 中定义还是先定义再添加为引用属性,这个函数严格来说都不属于 obj 对象。
  • 然而调用位置会使用 obj 上下文来引用函数,因此你可以说函数被调用时 obj 对象 "拥有" 或者 "包含" 函数引用。
  • 当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。因此 this.a 和 obj.a 是一样的。

  • 对象属性引用链只有上一层或者说最后一层在调用位置中起作用,例如
function foo() {console.log(this.a);
}var obj2 = {a: 111,foo,
};var obj1 = {a: 777,obj2,
};obj1.obj2.foo(); // 111// 对象 obj2 为最后一层
// obj1.obj2 仅为属性查找,并还没有开始调用
复制代码

函数脱离原上下文

  • 一个最常见 this 绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说他会应用默认绑定默认。
function foo() {console.log(this.a);
}var obj = {a: 2,foo,
};var bar = obj.foo; // 函数别名var a = "我是window下的a";bar(); // 我是window下的a
复制代码
  • 虽然 bar 是 obj.foo 的一个引用,但是实际上,它引用的是 foo 函数的本身,因此此时的 bar() 其实是一个普通的函数调用 因此应用了默认绑定。
  • 这实际上是重新定义了一个 bar 函数,和对象的结构一样,都是重新赋值,参考一下代码:
function foo() {console.log(this.a);
}var obj = {a: 2,foo,
};var { foo } = obj; // 这里相当于重新定义了一个函数或者说这是一个函数别名var a = "我是window下的a";foo(); // 我是window下的avar object = {moment: 777,age: 18,
};console.log(object); // {moment: 777, age: 18}
var { moment } = object;moment = "牛逼";console.log(moment); // 牛逼console.log(object); // {moment: 777, age: 18}
复制代码
  • 上面的代码,解构出来的变量 moment,实际上在全局作用域中创建了一个变量 moment 并赋值为 777,后面的直接修改变量不修改对象 object 中的属性 moment

函数作为参数

function foo() {console.log(this.a);
}function bar(fn) {// fn 其实是引用 foofn();
}var obj = {a: 777,foo,
};var a = "牛逼啊,这也行";bar(obj.foo); // 牛逼啊,这也行
复制代码
  • 参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,上面这段代码实际上就是以下代码的变体:
function foo() {console.log(this.a);
}function bar() {const fn = obj.foo;fn();
}var obj = {a: 777,foo,
};var a = "牛逼啊,这也行";bar(); // 牛逼啊,这也行
复制代码

显示绑定

  • 在 JavaScript 中,无论是宿主环境提供的一些函数还是你自己创建的函数,你都可以使用 call(...) 和 apply(...) 方法。
  • 他们的第一个参数是一个对象,是给this准备的,接着在调用函数时将其绑定到 this。因为你可以直接指定 this 的绑定对象,因此我们称之为 显示绑定
  • 这里 apply 和 call的语法规则就不讲了,有需要的可以去 mdn 官网查阅。

硬绑定

  • 硬绑定 这种方式可以把 this 强制绑定到指定的对象 (new 除外),既然有 硬绑定 ,自然也有 软绑定 ,在后文中我们会讲到。
function foo() {console.log(this.a);
}var obj = {a: 2,
};var bar = function () {foo.call(obj);
};bar(); // 2
setTimeout(bar, 1000); // 2// 硬绑定的 bar 不可能再修改他的 thisbar.call(window); // 2
复制代码
  • 用 apply 方法也一样的结果,只不过参数参数的方式不一样。
  • 而 bind 方法会返回一个硬编码的新函数,它会把你指定的参数设置为 this 的上下文调用原始参数。

API调用的 "上下文"

  • JavaScript 语言和 宿主环境 提供了许多内置函数,都提供了一个可选的参数,通常成为 上下文,其作用和 bind(...) 一样,确保你的回调函数使用指定的 this

function callback(element) {console.log(element, this.id);
}var obj = {id: "真不错",
};// 调用 foo(...) 时把 this 绑定到 obj 上
[1, 2, 3].forEach(callback, obj);
// 1 '真不错'  2 '真不错'  3 '真不错'// 俺 map 也一样[1, 2, 3].map(callback, obj);
// 1 '真不错'  2 '真不错'  3 '真不错'复制代码

new绑定

  • 在开始讲绑定之前,我想你已经知道了使用 new 来调用构造函数会执行什么操作,我们就再回顾一下吧:
  1. 在内存中创建一个新对象;
  2. 这个新对象内部的 [[prototype]] 特性 被赋值为构造函数的 prototype属性 (如果不了解这个也可以 点击这里);
  3. 构造函数中内部的 this 被赋值为这个新对象(即 this 指向新对象);
  4. 执行构造函数内部的代码(给新对象添加属性);
  5. 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象;

function Foo(moment) {this.moment = moment;
}var bar = new Foo(777);console.log(bar.a); // 777复制代码
  • 使用 new 来调用 Foo(...) 时,我们会构造一个新对象并把他绑定到 Foo(...) 调用中的 this 上。
  • 我们再来思考一下的代码输出结果是什么:
var mayDay = {moment: "moment",
};function Foo() {this.moment = 777;return mayDay;
}var bar = new Foo();console.log(bar.moment);
复制代码
  • 最终输出的结果是 moment,也就是 this 被绑定到了 mayDay 对象上,那么为什么会这样呢?

答案就在 new 的最后一条过程 "如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象" 这条规则上。

  • 换句话说就是,如果构造函数返回一个对象,则该对象将作为整个表达式的值返回,而传入的构造函数的 this 将会被抛弃。
  • 如果构造函数返回的是非对象类型,则忽略返回值,返回新创建的对象
var mayDay = {moment: "moment",
};function Foo() {this.moment = 777;return 111; // 这里的返回值变化了
}var bar = new Foo();console.log(bar.moment); // 777 输出的是新对象的 moment复制代码

类上下文

  • this 在 类中的表现与函数中类似,因为类本质上也是函数,但也有一些区别和注意事项。在类的构造函数中,this 是一个常规对象。类中所有非静态的方法都会被添加到 this 的原型中:

class Example {constructor() {const proto = Object.getPrototypeOf(this);console.log(Object.getOwnPropertyNames(proto));}first() {}second() {}static third() {} // 这里不在 this 上,在类本身上
}new Example(); // ['constructor', 'first', 'second']复制代码

箭头函数调用

箭头函数表达式的语法比函数表达式更简洁,并且没有自己的 thisarguments,supernew.target。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。正是因为箭头函数没有 this,自然而然的就不能使用 new 操作符了。


var moment = "moment";var bar = {moment: 777,general: function () {console.log(this.moment);},arrow: () => {console.log(this.moment);},nest: function () {var callback = () => {console.log(this.moment);};callback();},
};bar.general(); // 777
bar.arrow(); // moment
bar.nest(); // 777复制代码
  • 其中第一个普通函数的就是我们前面说的隐式绑定。
  • 第二个调用因为箭头函数没有自己的 this ,他会查找箭头函数上一层的的普通函数的 this,这时演变成了默认绑定了,是全局调用。
  • 第三个和第二个相似,但是它查找的上一层是函数 nest ,这是一个隐式绑定了,自然也就输出对象内部的 monent 。
  • 虽然箭头函数无法通过 callapplu , bind 绑定 this ,但是他可以绑定缓存箭头函数上层的普通函数的 this,例如:

var foo = {moment: 777,general: function () {console.log(this.moment);return () => {console.log("arrow:", this.moment);};},
};
var obj = {moment: "moment",
};
foo.general().call(obj); // 777  "arrow: 777 "
foo.general.call(obj)(); // 'moment' 'arrow:' 'moment'复制代码
  • 注意 settimeout 和 自执行函数 中的 this 指向 window

setTimeout(function foo() {console.log(this); // window
}, 0);(function () {console.log(this); // window
})();复制代码
  • 因为 settimeout这个方法是挂载在 window 对象上的,settimeout执行时,执行回调中的 this 指向调用 settimeout的对象,所以是 window

优先级

  • 如果某个调用位置可以应用多条规则该怎么办?为了解决这个问题就必须给这些规则设定优先级。显而易见,默认绑定的优先级是四条规则中最低的。

function foo() {console.log(this.a);
}var obj1 = {a: 666,foo,
};var obj2 = {a: 777,foo,
};obj1.foo(); // 666
obj2.foo(); // 777obj1.foo.call(obj2); // 777
obj2.foo.call(obj1); // 666复制代码
  • 通过以上代码可以看到,显示绑定 比 隐式绑定 优先级更高,也就是说在判断是应当先考虑是否可以存在显示绑定。

function foo(age) {this.age = age;
}var obj1 = {foo,
};var obj2 = {};obj1.foo(2);
console.log(obj1.age); // 2obj1.foo.call(obj2, 3);
console.log(obj2.age); // 3var bar = new obj1.foo(7);
console.log(obj1.age); // 2
console.log(bar.age); // 7复制代码
  • 可以看到 new绑定 比 隐式绑定 优先级更高,但是 new绑定 和显示绑定 谁的优先级更高呢?

  • 因为 new 和 call/apply 无法一起使用,因此无法通过 new foo.call(...) 来直接测试,但是我们可以使用硬绑定来测试他俩的优先级。


function foo(age) {this.age = age;
}var obj1 = {};var bar = foo.bind(obj1);
bar(2);console.log(obj1.age); // 2var baz = new bar(3);
console.log(obj1.age); // 2
console.log(baz.age); // 3复制代码
  • 结果出乎意料,bar 被绑定到 obj1 上,但是 new bar(3) 并没有像我们语句的那样把 obj1.age 修改为 3。相反, new 修改了硬绑定 (到 obj1的) 调用 bar(...)中的this。
  • 这是因为 new 调用时bind之后的函数,会忽略 bind 绑定的第一个参数,稍后我们会用 bind 方法的 ployfill 实现来讲清楚为什么会这样。
  • 综上所述,它们的优先级顺序分别是:
  1. new 调用;
  2. callapplybind 调用;
  3. 隐式绑定(对象方法调用);
  4. 默认绑定(普通函数调用);

bind的ployfill实现


Function.prototype.Bind = function (pointer) {if (typeof this !== "function") {throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");}// 将参数转换为数组const args = Array.prototype.slice.call(arguments, 1);const self = this;const NewFunc = function () {};const fBound = function () {return self.apply(// 如果是 new 操作符,则重新绑定thisthis instanceof NewFunc && pointer ? this : pointer,args.concat(Array.prototype.slice.call(arguments)));};NewFunc.prototype = this.prototype;fBound.prototype = new NewFunc();return fBound;
};复制代码
  • 其中,下面就是 new 修改 this 的相关代码:

this instanceof NewFunc && pointer ? this : pointer;// ... 以及;NewFunc.prototype = this.prototype;
fBound.prototype = new NewFunc();复制代码

软绑定

  • 之前我们讲到,硬绑定这种方法可以把 this 强制绑定到指定的对象(除了使用 new时),防止函数调用应用默认绑定规则。
  • 但是问题就在于硬绑定会大大降低函数的灵活性,使用硬绑定之后就无法使用隐式绑定或者显示绑定来修改 this 的能力,具体来看实现:

Function.prototype.softBind = function (object) {let fn = this;// 捕获所有的curried参数const curried = [].slice.call(arguments, 1);const bound = function () {return (fn.apply(!this || this === (window || global) ? object : this),curried.concat.apply(curried, arguments));};bound.prototype = Object.create(fn.prototype);return bound;
};function foo() {console.log(this.name);
}const obj = {name: "obj",
};
const obj2 = {name: "obj2",
};
const obj3 = {name: "obj3",
};const fooOBJ = foo.softBind(obj);
fooOBJ(); // objobj2.foo = foo.softBind(obj);
obj2.foo(); // obj2fooOBJ.call(obj3); // obj3setTimeout(obj2.foo, 1000); // obj复制代码
  • 可以看到,软绑定版本的 foo() 可以手动的将 this 绑定到不同的对象上。

你小子,又在偷偷学this指向相关推荐

  1. python都学什么啊-那些效率高的人都在偷偷学什么?

    原标题:那些效率高的人都在偷偷学什么? 提起潘石屹,你的印象是不是还停留在"SOHO中国"."房产大亨"的标签上?那你真是"图样图森破"! ...

  2. 我要偷偷学java,然后惊艳所有人

    <从零开始的java世界> 日子像流水一样,淌过了名叫光阴的小河. 这秋叶落了尽,以萧瑟的姿态迎接了冬天: 各位踏入编程的小伙伴们大家好,我叫小夜斗,从今天开始我要和大家一起学习java啦 ...

  3. 同事都在偷偷学Python,我莫名有了危机感...

    前两天凌晨,一个朋友很奔溃地给我发了一连串微信: 我特别能理解朋友这种生气和无力感. 在雇主相对强势的职场上,大多数个体就像是弱势群体,不管996还是715,大家只能被动地接受,没有讨价还价的空间. ...

  4. 偷偷学Python,怎么高空建楼(Python自动化办公实现批量替换Word)

    文章目录 前言 一.项目 二.工具 1.Word:域 2.Python 安装 代码 三.敲黑板 前言 最近在学Python,主要是用于自动化办公.在这之前,我没学过Python,硬要说有那就是两年前看 ...

  5. 你要偷偷学Python,然后惊艳所有人(类的属性和方法)

    一.类的结构 1.1:术语--实例 1.使用面向对象开发,第一步是设计类 2.使用类名()创建对象,创建对象的动作有两步 1)在内存中为对象分配空间2)调用初始化方法__init__为对象初始化 3. ...

  6. 偷偷学K3S,然后惊呆所有人

    k3s常用操作 修改主机名称 ### 三台主机分别修改主机名 hostnamectl set-hostname master01 hostnamectl set-hostname work01 hos ...

  7. 太强了!华为自研鸿蒙编程语言,行业大佬都已经偷偷学起来了

    华为开发者大会 2021(Together)上,Harmony OS 3 开发者预览版正式发布,华为鸿蒙又是一个历史性的进步啊,而且,华为还表示,将发布自研鸿蒙编程语言!!! 编程语言是什么? 这其实 ...

  8. 我要偷偷学Java,然后惊呆所有人!

    Java修饰符 1 访问控制修饰符 在java中可以使用访问控制符来保护类,变量,方法和构造方法的访问. * 默认为default,在同一包中可见,不使用任何修饰符,接口里的变量隐式声明为public ...

  9. python 10个100以内随机整数编辑_你要偷偷的学Python,然后惊呆所有人(第五天) - python阿喵

    标题无意冒犯,就是觉得这个广告挺好玩的 前言 前期回顾:你要偷偷学Python,然后惊呆所有人(第四天) 在第四天的时候,我们接触了Python的模块调用,这也是Python能够火起来的一大优势.我们 ...

最新文章

  1. 2022-2028年中国音像制品行业投资分析及前景预测报告
  2. 安卓java读取软件自身包名,android 获取第三方应用程序包名并启动 | 学步园
  3. SAP MM 并非奇怪现象之MB5B报表里期初库存余额或者期末库存余额为负数?
  4. python升级版本命令-CentOS7 下升级Python版本
  5. 实物贴图风格拟物图标素材,高逼格即显
  6. 已解决:fastclick插件在IOS系统上点击input需要双击或长按才有效
  7. [转帖]九句英语闯天下
  8. 人生苦短 须用Kotlin
  9. Wet Shark and Flowers(思维)
  10. Codeforces 364D Ghd(随机化)
  11. EfficientNet与EfficientDet的详解
  12. 跳转微信小程序和支付宝小程序
  13. python 复制并重命名文件_基于python实现复制文件并重命名
  14. 奇点临近:人类文明延续
  15. EC20 raspberry pi 树莓派 4g上网 发短信
  16. ProVideoPlayer 3 for Mac(多屏幕媒体播放器)
  17. 浏览器的标准模式、怪异模式
  18. Linux开启关闭内核打印信息
  19. 借花献佛!javafor循环跳出循环
  20. 逻辑漏洞挖掘之——逻辑漏洞概述

热门文章

  1. 「企业安全架构」EA874:信息安全架构
  2. baked lighting
  3. 巴斯卡三角形(Pascal)
  4. 没有一只蟹能活着爬出上海
  5. ffmpeg + cuda(cuvid) 硬解码+像素格式转换(cpu主导)实战
  6. 企业运维实战--lvs之DR模式负载均衡、keepalived、lvs高可用
  7. 三级分销系统到底是什么模式,三级分销特点
  8. matlab 信号插零,【 MATLAB 】MATLAB 实现模拟信号采样后的重建(二)零阶保持(ZOH)...
  9. CPU(Central Processing Unit,中央处理器)
  10. 魅族flashfire_高通平台所有黑砖(不开机)手机通用救砖方法