5.显式绑定扩展
上面提了很多call/apply
可以改变this
指向,但都没有太多实用性。下面来一起学几个常用的call与apply
使用。
题目5.1:apply求数组最值
JavaScript中没有给数组提供类似max和min函数,只提供了Math.max/min
,用于求多个数的最值,所以可以借助apply方法,直接传递数组给Math.max/min
const arr = [1,10,11,33,4,52,17] Math.max.apply(Math, arr) Math.min.apply(Math, arr) 复制代码
题目5.2:类数组转为数组
ES6
未发布之前,没有Array.from
方法可以将类数组转为数组,采用Array.prototype.slice.call(arguments)
或[].slice.call(arguments)
将类数组转化为数组。
题目5.3:数组高阶函数
日常编码中,我们会经常用到forEach、map
等,但这些数组高阶方法,它们还有第二个参数thisArg
,每一个回调函数都是显式绑定在thisArg
上的。
例如下面这个例子
const obj = {a: 10} const arr = [1, 2, 3, 4] arr.forEach(function (val, key){ console.log(`${key}: ${val} --- ${this.a}`) }, obj) 复制代码
答案
0: 1 --- 10 1: 2 --- 10 2: 3 --- 10 3: 4 --- 10 复制代码
关于数组高阶函数的知识可以参考: JavaScript之手撕高阶数组函数
6.new绑定
使用new
来构建函数,会执行如下四部操作:
- 创建一个空的简单
JavaScript
对象(即{}
); - 为步骤1新创建的对象添加属性
__proto__
,将该属性链接至构造函数的原型对象 ; - 将步骤1新创建的对象作为
this
的上下文 ; - 如果该函数没有返回对象,则返回
this
。
关于new更详细的知识,可以参考:JavaScript之手撕new
通过new来调用构造函数,会生成一个新对象,并且把这个新对象绑定为调用函数的this。
题目6.1:new绑定
function User(name, age) { this.name = name; this.age = age; } var name = 'Tom'; var age = 18; var zc = new User('zc', 24); console.log(zc.name) 复制代码
答案
zc 复制代码
题目6.2:属性加方法
function User (name, age) { this.name = name; this.age = age; this.introduce = function () { console.log(this.name) } this.howOld = function () { return function () { console.log(this.age) } } } var name = 'Tom'; var age = 18; var zc = new User('zc', 24) zc.introduce() zc.howOld()() 复制代码
这个题很难不让人想到如下代码,都是函数嵌套,具体解法是类似的,可以对比来看一下啊。
const User = { name: 'zc'; age: 18; introduce = function () { console.log(this.name) } howOld = function () { return function () { console.log(this.age) } } } var name = 'Tom'; var age = 18; User.introduce() User.howOld()() 复制代码
zc.introduce()
: zc是new创建的实例,this指向zc,打印zc
zc.howOld()()
: zc.howOld()返回一个匿名函数,匿名函数为默认绑定,因此打印18(阿包永远18)
答案
zc 18 复制代码
题目6.3:new界的天王山
new
界的天王山,每次看懂后,没过多久就会忘掉,但这次要从根本上弄清楚该题。
接下来一起来品味品味:
function Foo(){ getName = function(){ console.log(1); }; return this; } Foo.getName = function(){ console.log(2); }; Foo.prototype.getName = function(){ console.log(3); }; var getName = function(){ console.log(4); }; function getName(){ console.log(5) }; Foo.getName(); getName(); Foo().getName(); getName(); new Foo.getName(); new Foo().getName(); new new Foo().getName(); 复制代码
- 预编译
GO = { Foo: fn(Foo), getName: function getName(){ console.log(5) }; } 复制代码
- 分析后续执行
Foo.getName()
: 执行Foo上的getName方法,打印2
getName()
: 执行GO中的getName方法,打印4
Foo().getName()
Foo()
执行
// 修改全局GO的getName为function(){ console.log(1); } getName = function(){ console.log(1) } // Foo为默认绑定,this -> window // return window return this 复制代码
Foo().getName()
: 执行window.getName(),打印1
getName()
: 执行GO中的getName,打印1
- 分析后面三个打印结果之前,先补充一些运算符优先级方面的知识(图源:MDN)
从上图可以看到,部分优先级如下:new(带参数列表) = 成员访问 = 函数调用 > new(不带参数列表)
new Foo.getName()
首先从左往右看:new Foo
属于不带参数列表的new(优先级19),Foo.getName
属于成员访问(优先级20),getName()
属于函数调用(优先级20),同样优先级遵循从左往右执行。
Foo.getName
执行,获取到Foo上的getName
属性- 此时原表达式变为
new (Foo.getName)()
,new (Foo.getName)()
为带参数列表(优先级20),(Foo.getName)()
属于函数调用(优先级20),从左往右执行 new (Foo.getName)()
执行,打印2
,并返回一个以Foo.getName()
为构造函数的实例
这里有一个误区:很多人认为这里的
new
是没做任何操作的的,执行的是函数调用。那么如果执行的是Foo.getName()
,调用返回值为undefined
,new undefined
会发生报错,并且我们可以验证一下该表达式的返回结果。
console.log(new Foo.getName()) // 2 // Foo.getName {} 复制代码
可见在成员访问之后,执行的是带参数列表格式的new操作。
new Foo().getName()
- 同
步骤4
一样分析,先执行new Foo()
,返回一个以Foo
为构造函数的实例 Foo
的实例对象上没有getName
方法,沿原型链查找到Foo.prototype.getName
方法,打印3
new new Foo().getName()
从左往右分析: 第一个new不带参数列表(优先级19),new Foo()
带参数列表(优先级20),剩下的成员访问和函数调用优先级都是20
new Foo()
执行,返回一个以Foo
为构造函数的实例- 在执行成员访问,
Foo
实例对象在Foo.prototype
查找到getName
属性 - 执行
new (new Foo().getName)()
,返回一个以Foo.prototype.getName()
为构造函数的实例,打印3
new Foo.getName()
与new new Foo().getName()
区别:
new Foo.getName()
的构造函数是Foo.getName
new new Foo().getName()
的构造函数为Foo.prototype.getName
测试结果如下:
foo1 = new Foo.getName() foo2 = new new Foo().getName() console.log(foo1.constructor) console.log(foo2.constructor) 复制代码
输出结果:
2 3 ƒ (){ console.log(2); } ƒ (){ console.log(3); } 复制代码
通过这一步比较应该能更好的理解上面的执行顺序。
答案
2 4 1 1 2 3 3 复制代码
兄弟们,革命快要成功了,再努力一把,以后this都小问题啦。
7.箭头函数
箭头函数没有自己的this
,它的this
指向外层作用域的this
,且指向函数定义时的this
而非执行时。
this指向外层作用域的this
: 箭头函数没有this
绑定,但它可以通过作用域链查到外层作用域的this
指向函数定义时的this而非执行时
:JavaScript
是静态作用域,就是函数定义之后,作用域就定死了,跟它执行时的地方无关。更详细的介绍见JavaScript之静态作用域与动态作用域。
题目7.1:对象方法使用箭头函数
name = 'tom' const obj = { name: 'zc', intro: () => { console.log('My name is ' + this.name) } } obj.intro() 复制代码
上文说到,箭头函数的this
通过作用域链查到,intro
函数的上层作用域为window
。
答案
My name is tom 复制代码
题目7.2:箭头函数与普通函数比较
name = 'tom' const obj = { name: 'zc', intro:function () { return () => { console.log('My name is ' + this.name) } }, intro2:function () { return function() { console.log('My name is ' + this.name) } } } obj.intro2()() obj.intro()() 复制代码
obj.intro2()()
: 不做赘述,打印My name is tom
obj.intro()()
:obj.intro()
返回箭头函数,箭头函数的this
取决于它的外层作用域,因此箭头函数的this
指向obj
,打印My name is zc
题目7.3:箭头函数与普通函数的嵌套
name = 'window' const obj1 = { name: 'obj1', intro:function () { console.log(this.name) return () => { console.log(this.name) } } } const obj2 = { name: 'obj2', intro: ()=> { console.log(this.name) return function() { console.log(this.name) } } } const obj3 = { name: 'obj3', intro: ()=> { console.log(this.name) return () => { console.log(this.name) } } } obj1.intro()() obj2.intro()() obj3.intro()() 复制代码
obj1.intro()()
: 类似题目7.2
,打印obj1,obj1
obj2.intro()()
:obj2.intro()
为箭头函数,this
为外层作用域this
,指向window
。返回匿名函数为默认绑定。打印window,window
obj3.intro()()
:obj3.intro()
与obj2.intro()
相同,返回值为箭头函数,外层作用域intro
的this
指向window
,打印window,window
答案
obj1 obj1 window window window window 复制代码
题目7.4:new碰上箭头函数
function User(name, age) { this.name = name; this.age = age; this.intro = function(){ console.log('My name is ' + this.name) }, this.howOld = () => { console.log('My age is ' + this.age) } } var name = 'Tom', age = 18; var zc = new User('zc', 24); zc.intro(); zc.howOld(); 复制代码
zc
是new User
实例,因此构造函数User
的this
指向zc
zc.intro()
: 打印My name is zc
zc.howOld()
:howOld
为箭头函数,箭头函数this由外层作用域决定,且指向函数定义时的this,外层作用域为User
,this
指向zc
,打印My age is 24
题目7.5:call碰上箭头函数
箭头函数由于没有this
,不能通过call\apply\bind
来修改this
指向,但可以通过修改外层作用域的this
来达成间接修改
var name = 'window' var obj1 = { name: 'obj1', intro: function () { console.log(this.name) return () => { console.log(this.name) } }, intro2: () => { console.log(this.name) return function () { console.log(this.name) } } } var obj2 = { name: 'obj2' } obj1.intro.call(obj2)() obj1.intro().call(obj2) obj1.intro2.call(obj2)() obj1.intro2().call(obj2) 复制代码
obj1.intro.call(obj2)()
: 第一层函数为普通函数,通过call
修改this
为obj2
,打印obj2
。第二层函数为箭头函数,它的this
与外层this
相同,同样打印obj2
。obj1.intro().call(obj2)
: 第一层函数打印obj1
,第二次函数为箭头函数,call
无效,它的this
与外层this
相同,打印obj1
obj1.intro2.call(obj2)()
: 第一层为箭头函数,call
无效,外层作用域为window
,打印window
;第二次为普通匿名函数,默认绑定,打印window
obj1.intro2().call(obj2)
: 与上同,打印window
;第二层为匿名函数,call
修改this
为obj2
,打印obj2
答案
obj2 obj2 obj1 obj1 window window window obj2 复制代码
8.箭头函数扩展
总结
- 箭头函数没有
this
,它的this
是通过作用域链查到外层作用域的this
,且指向函数定义时的this
而非执行时。 - 不可以用作构造函数,不能使用
new
命令,否则会报错 - 箭头函数没有
arguments
对象,如果要用,使用rest
参数代替 - 不可以使用
yield
命令,因此箭头函数不能用作Generator
函数。 - 不能用
call/apply/bind
修改this
指向,但可以通过修改外层作用域的this
来间接修改。 - 箭头函数没有
prototype
属性。
避免使用场景
- 箭头函数定义对象方法
const zc = { name: 'zc', intro: () => { // this -> window console.log(this.name) } } zc.intro() // undefined 复制代码
- 箭头函数不能作为构造函数
const User = (name, age) => { this.name = name; this.age = age; } // Uncaught TypeError: User is not a constructor zc = new User('zc', 24); 复制代码
- 事件的回调函数
DOM中事件的回调函数中this已经封装指向了调用元素,如果使用构造函数,其this会指向window对象
document.getElementById('btn') .addEventListener('click', ()=> { console.log(this === window); // true }) 复制代码
9.综合题
学完上面的知识,是不是感觉自己已经趋于化境了,现在就一起来华山之巅一决高下吧。
题目9.1: 对象综合体
var name = 'window' var user1 = { name: 'user1', foo1: function () { console.log(this.name) }, foo2: () => console.log(this.name), foo3: function () { return function () { console.log(this.name) } }, foo4: function () { return () => { console.log(this.name) } } } var user2 = { name: 'user2' } user1.foo1() user1.foo1.call(user2) user1.foo2() user1.foo2.call(user2) user1.foo3()() user1.foo3.call(user2)() user1.foo3().call(user2) user1.foo4()() user1.foo4.call(user2)() user1.foo4().call(user2) 复制代码
这个题目并不难,就是把上面很多题做了个整合,如果上面都学会了,此题问题不大。
user1.foo1()、user1.foo1.call(user2)
: 隐式绑定与显式绑定user1.foo2()、user1.foo2.call(user2)
: 箭头函数与calluser1.foo3()()、user1.foo3.call(user2)()、user1.foo3().call(user2)
: 见题目4.8user1.foo4()()、user1.foo4.call(user2)()、user1.foo4().call(user2)
: 见题目7.5
答案:
var name = 'window' var user1 = { name: 'user1', foo1: function () { console.log(this.name) }, foo2: () => console.log(this.name), foo3: function () { return function () { console.log(this.name) } }, foo4: function () { return () => { console.log(this.name) } } } var user2 = { name: 'user2' } user1.foo1() // user1 user1.foo1.call(user2) // user2 user1.foo2() // window user1.foo2.call(user2) // window user1.foo3()() // window user1.foo3.call(user2)() // window user1.foo3().call(user2) // user2 user1.foo4()() // user1 user1.foo4.call(user2)() // user2 user1.foo4().call(user2) // user1 复制代码
题目9.2:隐式绑定丢失
var x = 10; var foo = { x : 20, bar : function(){ var x = 30; console.log(this.x) } }; foo.bar(); (foo.bar)(); (foo.bar = foo.bar)(); (foo.bar, foo.bar)(); 复制代码
突然出现了一个代码很少的题目,还乍有些不习惯。
foo.bar()
: 隐式绑定,打印20
(foo.bar)()
: 上面提到过运算符优先级的知识,成员访问与函数调用优先级相同,默认从左到右,因此括号可有可无,隐式绑定,打印20
(foo.bar = foo.bar)()
:隐式绑定丢失,给foo.bar
起别名,虽然名字没变,但是foo.bar
上已经跟foo
无关了,默认绑定,打印10
(foo.bar, foo.bar)()
: 隐式绑定丢失,起函数别名,将逗号表达式的值(第二个foo.bar)赋值给新变量,之后执行新变量所指向的函数,默认绑定,打印10
。
上面那说法有可能有几分难理解,隐式绑定有个定性条件,就是要满足
XXX.fn()
格式,如果破坏了这种格式,一般隐式绑定都会丢失。
题目9.3:arguments(推荐看)
var length = 10; function fn() { console.log(this.length); } var obj = { length: 5, method: function(fn) { fn(); arguments[0](); } }; obj.method(fn, 1); 复制代码
这个题要注意一下,有坑。
fn()
: 默认绑定,打印10arguments[0]()
: 这种执行方式看起来就怪怪的,咱们把它展开来看看:
arguments
是一个类数组,arguments
展开,应该是下面这样:
arguments: { 0: fn, 1: 1, length: 2 } 复制代码
arguments[0]
: 这是访问对象的属性0?0不好理解,咱们把它稍微一换,方便一下理解:
arguments: { fn: fn, 1: 1, length: 2 } 复制代码
- 到这里大家应该就懂了,隐式绑定,
fn
函数this
指向arguments
,打印2
题目9.4:压轴题(推荐看)
var number = 5; var obj = { number: 3, fn: (function () { var number; this.number *= 2; number = number * 2; number = 3; return function () { var num = this.number; this.number *= 2; console.log(num); number *= 3; console.log(number); } })() } var myFun = obj.fn; myFun.call(null); obj.fn(); console.log(window.number); 复制代码
fn.call(null)
或者fn.call(undefined)
都相当于fn()
obj.fn
为立即执行函数: 默认绑定,this
指向window
句一句的分析:
var number
: 立即执行函数的AO
中添加number
属性,值为undefined
this.number *= 2
:window.number = 10
number = number * 2
: 立即执行函数AO
中number
值为undefined
,赋值后为NaN
number = 3
:AO
中number
值由NaN
修改为3
- 返回匿名函数,形成闭包
- 此时的obj可以类似的看成以下代码(注意存在闭包):
obj = { number: 3, fn: function () { var num = this.number; this.number *= 2; console.log(num); number *= 3; console.log(number); } } 复制代码
myFun.call(null)
: 相当于myFun()
,隐式绑定丢失,myFun
的this
指向window
。依旧一句一句的分析:
var num = this.number
:this
指向window
,num = window.num = 10
this.number *= 2
:window.number = 20
console.log(num)
: 打印10number *= 3
: 当前AO
中没有number
属性,沿作用域链可在立即执行函数的AO
中查到number
属性,修改其值为9
console.log(number)
: 打印立即执行函数AO
中的number
,打印9
obj.fn()
: 隐式绑定,fn
的this
指向obj
继续一步一步的分析:
var num = this.number
:this->obj
,num = obj.num = 3
this.number *= 2
:obj.number *= 2 = 6
console.log(num)
: 打印num
值,打印3number *= 3
: 当前AO
中不存在number
,继续修改立即执行函数AO
中的number
,number *= 3 = 27
console.log(number)
: 打印27
console.log(window.number)
: 打印20
这里解释一下,为什么
myFun.call(null)
执行时,找不到number
变量,是去找立即执行函数AO
中的number
,而不是找window.number
: JavaScript采用的静态作用域,当定义函数后,作用域链就已经定死。(更详细的解释文章最开始的推荐中有)
答案
10 9 3 27 20 复制代码
总结
- 默认绑定: 非严格模式下
this
指向全局对象,严格模式下this
会绑定到undefined
- 隐式绑定: 满足
XXX.fn()
格式,fn
的this
指向XXX
。如果存在链式调用,this永远指向最后调用它的那个对象 - 隐式绑定丢失:起函数别名,通过别名运行;函数作为参数会造成隐式绑定丢失。
- 显示绑定: 通过
call/apply/bind
修改this
指向 new
绑定: 通过new
来调用构造函数,会生成一个新对象,并且把这个新对象绑定为调用函数的this
。- 箭头函数绑定: 箭头函数没有
this
,它的this
是通过作用域链查到外层作用域的this
,且指向函数定义时的this
而非执行时
后语
this到这里基本接近尾声了,松了一口气。 这篇文章写了好久,找资源,修改博文,各种乱七八糟的杂事,导致迟迟写不出满意的博文。有可能天生理科男的缘故吧,怎么写感觉文章都很生硬,但好在还是顺利写完了。
在文章的最后,感谢一下参考的博客和题目的来源
- 霖呆呆大佬
- 小夕大佬:嗨,你真的懂this吗?
- 渡一教育的题源
最后按照阿包惯例,附赠一道面试题:
var num = 10 var obj = {num: 20} obj.fn = (function (num) { this.num = num * 3 num++ return function (n) { this.num += n num++ console.log(num) } })(obj.num) var fn = obj.fn fn(5) obj.fn(10) console.log(num, obj.num) 复制代码