共计 7105 个字符,预计需要花费 18 分钟才能阅读完成。
前言
系列首发于公众号『前端进阶圈』,若不想错过更多精彩内容,请“星标”一下,敬请关注公众号最新消息。
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 mode
与non-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 来调用函数,会主动执行以下操作:
- 创立一个新对象
- 让新对象的
__proto__
(隐式原型) 等于函数的 prototype(显式原型) - 绑定 this, 让新象绑定于函数的 this 指向
- 判断返回值,如果返回值不是一个对象,则返回刚新建的新对象。
优先级
- 如果在某个调用地位利用多条规定该如何?那为了解决此问题,那就引申出了优先级问题。
- 毫无疑问,默认绑定的优先级是四条规定中最低的,能够先不思考它。
-
先来看看隐式绑定和显式绑定那个优先级更高?
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
- 是否在 new 中调用(new 绑定), this 指向新创建的对象
- 是否通过 call、apply(显示绑定),this 指向绑定的对象
- 是否在某个对象中调用(隐式绑定),this 指向绑定的上下文对象
- 如果都不是,则是默认绑定,在严格模式下,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 机制。
小结
-
判断 this 指向
- 是否在 new 中调用(new 绑定), this 指向新创建的对象
- 是否通过 call、apply(显示绑定),this 指向绑定的对象
- 是否在某个对象中调用(隐式绑定),this 指向绑定对象的上下文
- 如果都不是,则是默认绑定,在严格模式下,this 指向 undefined, 非严格模式下,this 指向全局对象。
- 箭头函数不会应用上述的四条规定,而是依据以后的词法作用域来决定 this 的。箭头函数会继承外层函数调用的 this 绑定(无论 this 绑定到什么)。与 ES6 之前的 self = this 的机制一样。
- 留神:对于默认绑定来说,决定 this 绑定对象的并不是调用地位是否处于严格模式,而是函数体是否处于严格模式。如果函数体处于严格模式,this 会被绑定到 undefined,否则 this 会被绑定到全局对象。
特殊字符形容:
- 问题标注
Q:(question)
- 答案标注
R:(result)
- 注意事项规范:
A:(attention matters)
- 详情形容标注:
D:(detail info)
- 总结标注:
S:(summary)
- 剖析标注:
Ana:(analysis)
-
提醒标注:
T:(tips)
往期举荐:
- 前端面试实录 HTML 篇
- 前端面试实录 CSS 篇
- JS 如何判断一个元素是否在可视区域内?
- Vue2、3 生命周期及作用?
- 排序算法:QuickSort
- 箭头函数与一般函数的区别?
- 这是你了解的 CSS 选择器权重吗?
- JS 中 call, apply, bind 概念、用法、区别及实现?
- 罕用位运算办法?
- Vue 数据监听 Object.definedProperty()办法的实现
- 为什么 0.1+ 0.2 != 0.3,如何让其相等?
- 聊聊对 this 的了解?
-
JavaScript 为什么要进行变量晋升,它导致了什么问题?
最初:
- 欢送关注『前端进阶圈』公众号,一起摸索学习前端技术 ……
- 公众号回复 加群 或 扫码, 即可退出前端交流学习群,一起高兴摸鱼和学习 ……
- 公众号回复 加好友,即可添加为好友