前言

系列首发于公众号『前端进阶圈』 ,若不想错过更多精彩内容,请“星标”一下,敬请关注公众号最新消息。

this 之谜揭底:从浅入深理解 JavaScript 中的 this 关键字(二)

调用位置

  • 在理解 this 的绑定过程之前,首先要理解调用位置调用位置就是函数在代码中被调用的位置(而不是声明的位置)
  • 通常来说,寻找调用位置就是寻找"函数被调用的位置", 最重要的要分析调用栈(就是为了到达当前执行位置所调用的所有函数)。运行代码时,调用器会在那个位置暂停,同时会在展示当前位置的函数调用列表,这就是调用栈。

绑定规则

  • 函数的调用位置决定了 this 的绑定对象,通常情况下分为以下几种规则:

默认绑定

  • 最常用的函数调用类型:独立函数调用。可把这条规则看到是无法应用其他规则时的默认规则。
function foo(){
console.log(this.a);
}
var a = 2;
foo(); // 2
  • 当调用 foo() 时,this.a 被解析成了全局变量 a。为什么?

    • 因为在上述代码中,函数调用时应用了this 的默认绑定,因此 this 指向全局对象。(要理解 this,就要先理解调用位置)
  • 如果使用严格模式(strict mode),那全局对象将无法使用默认绑定,因此 this 会绑定到 undefined。
function foo(){
"use strict";
console.log(this.a);
}
var a = 2;
foo(); // Type: this is undefined
  • 虽然 this 的绑定规则完全取决于调用位置,但是只有 foo() 运行在非 strict mode下时,默认绑定才能绑定到全局对象; 严格模式下与 foo() 的调用位置无关。
function foo(){
console.log(this.a);
} var a = 2; (function (){
"use strict"; foo(); // 2
})
  • 通常情况下,尽量减少在代码中混合使用 strict modenon-strict mode,尽量减少在代码中混合使用 strict mode 和 non-strict mode。

隐式绑定

  • 另一条规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或包裹。
  • 考虑以下代码:
function foo() {
console.log(this.a); // 2
} var obj = {
a: 2,
foo: foo
} obj.foo();
  • 上述代码中,调用位置使用 obj 的上下文来引用函数,可以说函数被调用时 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
  • 隐式丢失:在被隐式绑定的函数会丢失绑定对象,也就是说它会默认绑定,从而把 this 绑定到全局对象或 undefined 上,这取决于是否是严格模式。
  • 如下代码:
function foo() {
console.log( this.a );
} var obj = {
a: 2,
foo: foo
}; var bar = obj.foo; // 函数别名! var a = "oops, global"; // a 是全局对象的属性 bar(); // "oops, global"
  • 还有一种奇怪的方式,就是在传入回调函数时隐式丢失
function foo() {
console.log( this.a );
} function doFoo(fn) {
// fn其实引用的是 foo fn(); // <-- 调用位置!
} var obj = {
a: 2,
foo: foo
}; var a = "oops, global"; // a 是全局对象的属性 doFoo( obj.foo ); // "oops, global"
  • 在我们传入函数时也会被隐式赋值。
  • 那如果传入的函数不是自定义的函数,而是语言内置的函数呢?结果还是一样的,没有区别
function foo() {
console.log( this.a );
} var obj = {
a: 2,
foo: foo
}; var a = "oops, global"; // a 是全局对象的属性 setTimeout( obj.foo, 100 ); // "oops, global"

显示绑定

  • 那我们不想在对象内部包含函数引用,而是想在某个对象上强制调用函数,该如何操作?

    • 那就必须要使用 call() 和 apply()。第一个参数是一个对象,也就是需要绑定的对象,第二个参数传入的参数,而两者之间的区别就在于第二个参数,call 的第二个参数是一个个参数,而 apply 则是一个参数数组。
// call()
function foo() {
console.log( this.a );
} var obj = {
a:2
}; foo.call( obj ); // 2 // apply()
function foo(something) {
console.log( this.a, something );
return this.a + something;
} var obj = {
a:2
}; var bar = function() {
return foo.apply( obj, arguments );
}; var b = bar( 3 ); // 2 3
console.log( b ); // 5

new绑定

  • 在传统的语言中,构造函数时一个特殊方法,使用 new 初始化需要调用的类,通常形式下是 let something = new MyClass();
  • 在使用 new 来调用函数,会自动执行以下操作:
    1. 创建一个新对象
    2. 让新对象的 __proto__(隐式原型) 等于函数的 prototype(显式原型)
    3. 绑定 this, 让新象绑定于函数的 this 指向
    4. 判断返回值,如果返回值不是一个对象,则返回刚新建的新对象。

优先级

  • 如果在某个调用位置应用多条规则该如何?那为了解决此问题,那就引申出了优先级问题。
  • 毫无疑问,默认绑定的优先级是四条规则中最低的,可以先不考虑它。
  • 先来看看隐式绑定和显式绑定那个优先级更高?
function foo() {
console.log( this.a );
} var obj1 = {
a: 2,
foo: foo
}; var obj2 = {
a: 3,
foo: foo
}; // 隐式绑定
obj1.foo(); // 2
obj2.foo(); // 3 // 显式绑定
obj1.foo.call( obj2 ); // 3
obj2.foo.call( obj1 ); // 2
  • 可以看出,显式绑定的优先级更高,也就是说在判断时应当考虑是否可以应用显式绑定。
  • 再来看看new绑定和隐式绑定的优先级?
function foo(something) {
this.a = something;
} var obj1 = {
foo: foo
}; var obj2 = {}; // 隐式绑定
obj1.foo( 2 );
console.log( obj1.a ); // 2 obj1.foo.call( obj2, 3 );
console.log( obj2.a ); // 3 // new绑定
var bar = new obj1.foo( 4 );
console.log( obj1.a ); // 2
console.log( bar.a ); // 4
  • 可以看出,new 绑定比隐式绑定的优先级更高,但 new 绑定和显式绑定谁的优先级更高呢?
  • new 与 call/apply 无法一起使用,因此无法通过 new foo.call(obj1) 来进行测试,但可以通过硬绑定来测试他两的优先级。
  • 硬绑定:Function.prototype.bind(...) 会创建一个新的包装函数,这个函数会忽略当前的this绑定(无论绑定的对象是什么),并把我们提供的对象绑定到this上。
  • 这样看起来硬绑定(也是显式绑定的一种)似乎比 new 绑定的优先级更高,无法使用 new 来控制 this 绑定。
function foo(something) {
this.a = something;
} var obj1 = {}; var bar = foo.bind( obj1 );
bar( 2 );
console.log( obj1.a ); // 2 var baz = new bar(3);
console.log( obj1.a ); // 2
console.log( baz.a ); // 3
  • 出乎意料! bar 被硬绑定到 obj1 上,但是 new bar(3) 并没有像我们预计的那样把 obj1.a 修改为 3。相反, new 修改了硬绑定(到 obj1 的)调用 bar(..) 中的 this。因为使用了 new 绑定,我们得到了一个名字为 baz 的新对象,并且 baz.a 的值是 3。
  • 硬绑定中的bind(...) 的功能之一就是可以把除了第一个参数(第一个参数用于绑定this)之外的其他参数传递给下层的函数(这种技术称为"部分应用",是"柯里化"的一种)。
function foo(p1,p2) {
this.val = p1 + p2;
} // 之所以使用 null 是因为在本例中我们并不关心硬绑定的 this 是什么
// 反正使用 new 时 this 会被修改
var bar = foo.bind( null, "p1" ); var baz = new bar( "p2" ); baz.val; // p1p2
  • 判断this

    1. 是否在 new 中调用(new 绑定), this 指向新创建的对象
    2. 是否通过 call、apply(显示绑定),this 指向绑定的对象
    3. 是否在某个对象中调用(隐式绑定),this 指向绑定的上下文对象
    4. 如果都不是,则是默认绑定,在严格模式下,this 指向 undefined, 非严格模式下,this 指向全局对象。
  • 优先级问题
    • 显式绑定:call()、apply()。(硬绑定也是显式绑定的其中一种: bind())
    • new 绑定: new Foo()
    • 隐式绑定: obj.foo();
    • 默认绑定: foo();
  • 排序:显式绑定 > new 绑定 > 隐式绑 定 > 默认绑定

绑定例子

被忽略的this

  • 如果你把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind,这些值在调用时会被忽略,实际应用的是默认绑定规则:
function foo() {
console.log( this.a );
} var a = 2; foo.call( null ); // 2
  • 那在什么情况下会传入 null 呢?

    • 一种非常常见的做法是使用 apply(..) 来“展开”一个数组,并当作参数传入一个函数。
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
  • 但总是用 null 来忽略 this 绑定可能会产生一些副作用。
  • 更安全的this
    • DMZ(demilitarized zone)空委托对象
  • 在 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

间接引用

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 会被绑定到全局对象。

软绑定

  • 硬绑定这种方式可以把 this 强制绑定到指定的对象(除了使用 new 时),防止函数调用应用默认绑定规则。使用硬绑定会大大降低函数的灵活性,使用硬绑定之后就无法使用隐式绑定或显示绑定来修改 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;
};
}
  • 实现软绑定功能:
function foo() {
console.log("name: " + this.name);
} var obj = { name: "obj" },
obj2 = { name: "obj2" },
obj3 = { name: "obj3" }; var fooOBJ = foo.softBind( obj ); fooOBJ(); // name: obj obj2.foo = foo.softBind(obj);
obj2.foo(); // name: obj2 <---- 看!!! fooOBJ.call( obj3 ); // name: obj3 <---- 看! setTimeout( obj2.foo, 10 );
// name: obj <---- 应用了软绑定
  • 可以看到,软绑定的 foo() 可手动将 this 绑定到 obj2 或 obj3 上,但如果应用默认绑定,则会将 this 绑定到 obj。

this 词法

  • 在 ES6 中出现了一种无法使用这些规则的特殊函数类型:箭头函数
  • 箭头函数不适用 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 也不行!)
  • 在 ES6 之前,我们也有使用和箭头函数一样的模式,如下代码:
function foo() {
var self = this; // this 快照
setTimeout( function(){
console.log( self.a );
}, 100 );
} var obj = {
a: 2
}; foo.call( obj ); // 2
  • 虽然 self = this 和箭头函数看起来都可以取代 bind(..),但是从本质上来说,它们想替代的是 this 机制。

小结

  1. 判断 this 指向

    1. 是否在 new 中调用(new 绑定), this 指向新创建的对象
    2. 是否通过 call、apply(显示绑定),this 指向绑定的对象
    3. 是否在某个对象中调用(隐式绑定),this 指向绑定对象的上下文
    4. 如果都不是,则是默认绑定,在严格模式下,this 指向 undefined, 非严格模式下,this 指向全局对象。
  2. 箭头函数不会使用上述的四条规则,而是根据当前的词法作用域来决定 this 的。箭头函数会继承外层函数调用的 this 绑定(无论 this 绑定到什么)。与 ES6 之前的 self = this 的机制一样。
  3. 注意:对于默认绑定来说,决定 this 绑定对象的并不是调用位置是否处于严格模式,而是函数体是否处于严格模式。如果函数体处于严格模式,this 会被绑定到 undefined,否则this 会被绑定到全局对象。

特殊字符描述:

  1. 问题标注 Q:(question)
  2. 答案标注 R:(result)
  3. 注意事项标准:A:(attention matters)
  4. 详情描述标注:D:(detail info)
  5. 总结标注:S:(summary)
  6. 分析标注:Ana:(analysis)
  7. 提示标注:T:(tips)

往期推荐:

最后:

  • 欢迎关注 『前端进阶圈』 公众号 ,一起探索学习前端技术......
  • 公众号回复 加群扫码, 即可加入前端交流学习群,一起快乐摸鱼和学习......
  • 公众号回复 加好友,即可添加为好友

this 之谜揭底:从浅入深理解 JavaScript 中的 this 关键字(二)的更多相关文章

  1. JavaScript基础知识从浅入深理解(一)

    JavaScript的简介 javascript是一门动态弱类型的解释型编程语言,增强页面动态效果,实现页面与用户之间的实时动态的交互. javascript是由三部分组成:ECMAScript.DO ...

  2. 从浅入深——理解JSONP的实现原理

    由于浏览器的安全性限制,不允许AJAX访问 协议不同.域名不同.端口号不同的 数据接口,浏览器认为这种访问不安全: 可以通过动态创建script标签的形式,把script标签的src属性,指向数据接口 ...

  3. 浅入深出Vue:工具准备之PostMan安装配置及Mock服务配置

    浅入深出Vue之工具准备(二):PostMan安装配置 由于家中有事,文章没顾得上.在此说声抱歉,这是工具准备的最后一章. 接下来就是开始环境搭建了~尽情期待 工欲善其事必先利其器,让我们先做好准备工 ...

  4. 浅入深出Vue系列

    浅入深出Vue导航 导航帖,直接点击标题即可. 文中所有涉及到的资源链接均在最下方列举出来了. 前言 基础篇 浅入深出Vue:工具准备之WebStorm搭建及配置 浅入深出Vue之工具准备(二):Po ...

  5. 浅入深出之Java集合框架(上)

    Java中的集合框架(上) 由于Java中的集合框架的内容比较多,在这里分为三个部分介绍Java的集合框架,内容是从浅到深,如果已经有java基础的小伙伴可以直接跳到<浅入深出之Java集合框架 ...

  6. 浅入深出之Java集合框架(中)

    Java中的集合框架(中) 由于Java中的集合框架的内容比较多,在这里分为三个部分介绍Java的集合框架,内容是从浅到深,如果已经有java基础的小伙伴可以直接跳到<浅入深出之Java集合框架 ...

  7. 浅入深出之Java集合框架(下)

    Java中的集合框架(下) 由于Java中的集合框架的内容比较多,在这里分为三个部分介绍Java的集合框架,内容是从浅到深,哈哈这篇其实也还是基础,惊不惊喜意不意外 ̄▽ ̄ 写文真的好累,懒得写了.. ...

  8. 浅入深出Vue:环境搭建

    浅入深出Vue:环境搭建 工欲善其事必先利其器,该搭建我们的环境了. 安装NPM 所有工具的下载地址都可以在导航篇中找到,这里我们下载的是最新版本的NodeJS Windows安装程序 下载下来后,直 ...

  9. 浅入深出Vue:工具准备之WebStorm安装配置

    浅入深出Vue之工具准备(一):WebStorm安装配置 工欲善其事必先利其器,让我们先做好准备工作吧 导航篇 WebStorm安装配置 所有工具的下载地址都可以在导航篇中找到,这里我们下载的是最新版 ...

  10. 浅入深出Vue:前言

    浅入深出Vue系列文章 之前大部分是在做后端,后来出于某些原因开始接触Vue.深感前端变化之大,各种工具.框架令人眼花缭乱.不过正是这些变化,让前端开发更灵活. 博主在刚开始时,参考官网的各个步骤以及 ...

随机推荐

  1. 「C++」复杂模拟【壹】

    建议开启目录食用 阅读本文之前建议您先看这里,如果您已经看完了,那么就可以放心大胆的学习本文了. 我认为其实本文的难度还是比较大的,今天我们题是来自山东省省选,所以建议大家谨慎阅读,如果您是专业程序员 ...

  2. python globals()[]将字符串转化类,并通过反射执行方法

    背景: 通过关键字设计ui自动化框架,将测试用例及其步骤存放到excel文件:其中步骤中包含了封装好的关键字方法,如打开浏览器.输入页面操作等,关键字保存的内容:具体类实例.方法 通过excel获取到 ...

  3. Ubuntu访问samba共享文件

    Ubuntu访问samba共享文件 参考:https://www.cnblogs.com/Wolf-Dreams/p/11241198.html 做法 安装samba-client.cifs-util ...

  4. C#的多线程UI窗体控件显示方案 - 开源研究系列文章

    上次编写了<LUAgent服务器端工具>这个应用,然后里面需要新启动一个线程去对文件进行上传到FTP服务器,但是新线程里无法对应用主线程UI的内容进行更改,所以就需要在线程里设置主UI线程 ...

  5. 新品来袭,全国产ARM+FPGA--"RK3568J+Logos-2"工业核心板,让您的硬件设计“更简单”!

    如需选购,请登录创龙科技天猫旗舰店: tronlong.tmall.com! 欢迎加入RK3568J技术交流群:567208221 欢迎加入Logos-2技术交流群:311416997 更多产品详情以 ...

  6. 全国产!全志T3+Logos FPGA开发板(4核ARM Cortex-A7)规格书

    评估板简介 创龙科技TLT3F-EVM是一款基于全志科技T3四核ARM Cortex-A7 + 紫光同创Logos PGL25G/PGL50G FPGA设计的异构多核国产工业评估板,ARM Corte ...

  7. 开发一个题库系统App和小程序的心得

    序言 对于一名开发者来说,独自开发一款小程序与App,也许总会有一些疑问: 1. 需要掌握哪些技术? 答:java.vue.及常规Linux命令 2. 需要多少成本? 答:服务器购买,云服务器新人50 ...

  8. aop的两种配置方法

    一.实现接口并重写方法 实现org.aopalliance.intercept.MethodInterceptor接口,这是AOP Alliance规范中的接口,Spring AOP支持它.这种方式比 ...

  9. GCC8 编译优化 BUG 导致的内存泄漏

    1. 背景 1.1. 接手老系统 最近我们又接手了一套老系统,老系统的迭代效率和稳定性较差,我们打算做重构改造,但重构周期较长,在改造完成之前还有大量的需求迭代.因此我们打算先从稳定性和迭代效率出发做 ...

  10. Mac 完整卸载mysql

    依次执行 cd ~ sudo rm /usr/local/mysql sudo rm -rf /usr/local/mysql* sudo rm -rf /Library/StartupItems/M ...