深入理解JavaScript设计模式之call,apply,this
前言:
最近在看曾探老师的《JavaScript设计模式》
,说实话,可能是我基础还不够扎实,书里的很多内容对我来说不是“一看就懂”,而是“看了三遍才懂一点点”。特别是像代码逻辑、概念解释这些地方,常常是一次看不懂,就来两次;两次还迷糊,就再来一次。但我慢慢也想通了,学习本来就是一个不断重复的过程。每一次重读,都能从字里行间中发现新的理解,就像挖宝藏一样,每次翻一翻都有新收获。为了让自己记得更牢,也为了让以后复习的时候不那么枯燥,我在反复阅读第二章“this、call、apply”
之后,试着用自己的话,把知识点编成了一个小故事的形式,讲给自己听。
我想,这不仅是一种学习方式,也是我对这本书的一种致敬。
目录
- 前言:
- this找家
- call 和 apply 导游
- this的指向
- 第一站、作为对象的方法被调用(this回到家)
- 第二站、普通函数调用(this离家出走)
- 第三站、构造器调用(this自己当爹)
- 第四站、Function.prototype.call 或 Function.prototype.apply 调用
- 修复 document.getElementById 的 this
- call和Apply
- Apply序
- Apply正文:
- call序
- call正文:
- 注意:
- Function.prototype.bind
- 改变 this 指向:为函数找到真正的家
- 借用其他对象的方法:杜鹃鸟的智慧
- 完~
this找家
想象一下,this
就像是一个四处旅行的人,他总在寻找它的“家”,但是问题来了,这个人(this)
的家并不固定,取决于他是怎么被邀请这个地方的,在javaScript
中,this
就是一个旅行者,他的指向完全依赖于函数的调用方式。
- 如果是对象的方法调用:比如
obj.myMethod()
,那么this
的家就安在obj
里面。- 如果是作为构造函数调用:比如
new MyClass()
,这时候this
就有了自己的新家,一个新的对象实例- 如果是直接调用函数,比如
myFunction()
,那么严格模式下,this
就会变得无家可归,即undefined
,而在非严格模式下,则默认回到了全局对象浏览器的window
的怀抱。
call 和 apply 导游
现在,比如我们的this
旅行者到了一个陌生的城市,不知道如何找到正确的家,这时候就可以请call
和apply
两位热心的导游出现了。
call
这个导游很细心,他会亲自带着this
去正确的地方,并且还允许你指定额外的参数列表,就像myFunction.call(context,arg1,arg2)
。在这里,context
就是我们希望this
去的家,而后面参数就是这次旅行所需要的装备。
apply
与call
不同的是,apply
更喜欢一次性把所有的东西都准备好再出发,所以除了第一个参数用来指定this
应该去的"家"外,它接收第二个参数为一个数组或类数组对象,里面包含了所需要传递给函数的参数,如myFunction.apply(context,[arg1,arg2])
,总之,当你想要控制this
的指向,或者需要特定的方式传递参数的时候,call
和apply
就是你的得力导游,他们不仅能帮this
找到回家的路,还能确保旅途愉快顺利。
this的指向
第一站、作为对象的方法被调用(this回到家)
有一天,this被一个叫做obj的大叔收留了,成为了大叔家的佣人。
var obj = {a: 1,getA: function() {console.log(this === obj); // trueconsole.log(this.a); // 1}
};obj.getA();
着obj家里,this每天多的都很开心,this开心的喊:“我终于找到加啦!我是obj家的孩子”,因为他是在obj家里被调用的,所以他就是的孩子。
第二站、普通函数调用(this离家出走)
但是有一天,this不小心跑出了obj的家,在外面遇到一个叫getName的陌生人:
var getName = obj.getName;
getName(); // 输出 undefined 或 globalName
😨这时候的this已经不是obj的孩子了,他又变成了流浪汉,回到了全局大哥window家,不过别担心,如果this在全局如果window家过的不开心,可以开启"use strict"【严格模式】模式,this更加自尊自爱,宁愿当个“无家可归的孤独者”,也不愿意随便认爹! 很有骨气吧,用孤独换的骨气 哈哈哈。
第三站、构造器调用(this自己当爹)
后来啊,有骨气的this长大了,学会了独立,决定自己当爸爸!
function Person(name) {this.name = name;
}
var tom = new Person('Tom');
console.log(tom.name); // Tom
结果this发现,自己辛辛苦苦当的爹,娃娃居然被别人抢走了。
第四站、Function.prototype.call 或 Function.prototype.apply 调用
这时候,两位神秘人物登场了,他们就是大名鼎鼎JavaScript剧场的导演call和apply!,他们告诉this:“this你很坚强,我们被你打动了,现在开始,你想演谁都可以,我帮你换身份。”,于是:
var obj1 = { name: '钢铁侠' };
var obj2 = { name: '蜘蛛侠' };
function sayHi() {console.log("我是" + this.name);
}
sayHi.call(obj1); // 我是钢铁侠
sayHi.apply(obj2); // 我是蜘蛛侠
this得到两位大哥的帮助,开始在不同的角色之间切换,一会是钢铁侠,一会是蜘蛛侠,忙的不亦乐乎,体验那种既可以当英雄,又可以有家的感觉。
有一天,this在div家做客,突然被一个叫做callback的函数骗走了身份信息:
<div id="div1">我是一个 div</div>
<script>
document.getElementById('div1').onclick = function () {alert(this.id); // div1 ✅var callback = function () {alert(this.id); // window ❌}callback();
};
</script>
这个时候,this又跑回了window家,而它本应该属于哪个div,怎么办呢,this想了个办法:
var that = this;
var callback = function () {alert(that.id); // div1 ✅
}
就像this孩子给自己办一张身份证,再也不怕他迷路啦。
修复 document.getElementById 的 this
有一次,this被借去演习,结果却跑到了document.getElementById里面,导致整个剧组都乱了套。
<html> <body> <div id="div1">我是一个 div</div> </body> <script> var getId = document.getElementById; getId( 'div1' ); // 报错,this不在指向document</script>
</html>
getId( ‘div1’ );就会报错,this不在指向document,原来this应该是docuement家的人,结果却被当成window使用了,apply导演不舍得this,就决定动用自己的能力,立刻修复:
document.getElementById = (function( func ){ return function(){ return func.apply( document, arguments ); }
})( document.getElementById );
var getId = document.getElementById;
var div = getId( 'div1' );
alert (div.id); // 输出: div1
终于,this回到了正确的家庭,拯救了整个剧组,经过这次惨痛的教训,导演对这次事故进行分析总结:
导演发现,在Chrome执行过后发现,var getId = document.getElementById
抛出了一个异常,因为许多的引擎document.getElementById
方法的内部实现张需要用到this
。这个this
本来被期望指向document
,当getElementById
方法作为document
对象的属性被调用时,方法内部的this
确实是指向document
的,但是当getId
来引用document.getElementById
之后,再调用getId
,此时就成了普通函数调用,函数内部的this
指向了Window
,而不是原来的document
。
导演发现可以使用apply
,把document
当作this
传入getId
函数,帮助“修正”this
:
<script>document.getElementById = (function (func) {return function () {return func.apply(document, arguments);};})(document.getElementById);var getId = document.getElementById;var div = getId("div1");alert(div.id); // 输出: div1
</script>
终于通过导演的不懈努力与分析让this回到了正确的家庭总结了整个剧组,甚至还总结了口诀:
this
是谁调用了我,我就跟谁混。
call/apply
是this
的导演,想让this
演谁都可以。
发生了上面的事情之后,this学会了如何在javaScript世界中找到自己的归属,它不再轻易迷路,也不再害怕被借来借去,从此要记住:
- 多问问
this:“whu ar you ?" 你是谁?
- 多请
call
和apply
来帮忙。- 别忘了用
bind
或that=this
给this
上个保险!
call和Apply
Apply序
apply
是一个豪放派的导演,他习惯把所有演员打包成一个团队(数组或类数组),然后一次性推到主演面前。
func.apply(thisArg, [arg1, arg2, ...]);
尽管风格不同,但是目标是一致的,帮助函数找到正确的舞台背景(改变this的指向),并让每个演员都能发挥最大的作用(正确的传递)。
Apply正文:
apply接收两个参数,第一个参数指定了函数体内this的指向,第二个参数为一个带下标的集合,这个集合可以为数组,也可以为类数组,apply
方法把这个结合中的元素作为参数传递给被调用的函数:
var func = function( a, b, c ){ alert ( [ a, b, c ] ); // 输出 [ 1, 2, 3 ]
};
func.apply( null, [ 1, 2, 3 ] );
在这段代码中,参数 1、2、3
被放在数组中一起传入 func
函数,它们分别对应 func
参数列表中的 a、b、c
。
call序
call
是一位细心讲究的导演,他喜欢把演员(参数)一个个亲自介绍给主演(函数),确保每个演员都有明确的角色。
func.call(thisArg, arg1, arg2, ...);
call正文:
call
传入的参数数量不固定,跟apply相同的是,第一个参数也是代表函数体内的this
指向,从第二个参数开始往后,每个参数被依次传入函数:
var func = function( a, b, c ){ alert ( [ a, b, c ] ); // 输出 [ 1, 2, 3 ]
};
func.call( null, 1, 2, 3 );
当调用一个函数的时候,JavaScript
的解释器并不会计较形参和实参的数量、类型、以及顺序上的区别,JavaScript
的参数在内部就用一个数组来表示的,从这个意义上说,apply
比call
使用率更高,不必关心具体多少个参数被传入函数,只要作用apply
一股脑地推过去即可。
注意:
当使用call
或者apply
的时候,如果我们传入的第一个参数为null
,函数体内的this
会默认值想宿主对象,在浏览器中则是windows
。
Function.prototype.bind
Function.prototype.bind
是一个秘密装备,能让你随心所欲的控制函数内部的this指向,即使在遥远的地方调用这个函数时也能保持“它”的初心,首先我们要打造一个属于自己的 Function.prototype.bind
我们先把 func
函数的引用保存起来,然后返回一个新的函数。当我们在将来执行 func
函数时,实际上先执行的是这个刚刚返回的新函数。在新函数内部,self.apply( context, arguments )
这句代码才是执行原来的 func
函数,并且指定 context
对象为 func
函数体内的 this
。
Function.prototype.bind = function(context) { var self = this; // 保存原函数,就像把宝剑藏在腰间return function() { // 返回一个新的函数,这就是我们的新武器return self.apply(context, arguments); // 当使用这把宝剑时,它会自动指向正确的方向}
}; var obj = { name: 'sven' }; // 我们的英雄 sven
var func = function() { alert(this.name); // 输出:sven
}.bind(obj); func(); // 英雄登场!
上面代码中,bind
就像给函数穿上了一层魔法铠甲,无论什么时候,只要调用它,它就会带着obj
的这个身份出现。但是,真正的超级英雄不能只有一技之长!我们需要让 bind
更加强大,让它不仅能绑定 this
,还能预先填入一些参数。这就像是给我们的宝剑装上了“魔法石”
,让它变得更强大:
Function.prototype.bind = function() { var self = this, // 保存原函数context = [].shift.call(arguments), // 需要绑定的 this 上下文,也就是英雄的身份args = [].slice.call(arguments); // 把剩下的参数转换成数组,作为魔法石return function() { // 返回一个新的函数,这是我们的终极武器return self.apply(context, [].concat.call(args, [].slice.call(arguments))); // 组合两次传入的参数,作为新函数的参数}
}; var obj = { name: 'sven' }; // 英雄 sven 再次登场
var func = function(a, b, c, d) { alert(this.name); // 输出:svenalert([a, b, c, d]); // 输出:[1, 2, 3, 4]
}.bind(obj, 1, 2); func(3, 4); // 召唤英雄,带上所有的魔法石!
改变 this 指向:为函数找到真正的家
在一个戏剧性的场景中,有一名为getName
的函数,它渴望知道自己到底属于哪个家族:
var obj1 = { name: 'sven' };
var obj2 = { name: 'anne' };
window.name = 'global';
function getName() {alert(this.name);
}
getName(); // 输出: global
// 现在,Call 导演登场,帮助 getName 找到了它的真正归属——obj1 家族
getName.call(obj1); // 输出: sven
通过call
和apply
大导演的帮助下,getName
终于找到了它真正的家!
借用其他对象的方法:杜鹃鸟的智慧
JavaScript
中也有一种"借壳生蛋"的艺术,就像杜鹃鸟将自己的蛋托付给其他鸟类孵化一样,这里call
和apply
也能帮我们借用其他对象的方法来完成一些任务。
例1:
var A = function( name ){ this.name = name;
};
var B = function(){ A.apply( this, arguments );
};
B.prototype.getName = function(){ return this.name;
};
var b = new B( 'sven' );
console.log( b.getName() ); // 输出: 'sven'
上面例子中,B
借用了A
的构造函数逻辑来初始化自己的实例属性,可以说是继承了A
的部分功能,准确来说是构造函数继承
或借用构造函数
。
函数的参数列表 arguments
是一个类数组对象,虽然它也有“下标”,但它并非真正的数组,所以也不能像数组一样,进行排序操作或者往集合里添加一个新的元素。
我们常常会借用 Array.prototype
对象上的方法。比如想往 arguments
中添加一个新的元素,通常会借用Array.prototype.push
:
(function() {Array.prototype.push.call(arguments, 3);console.log(arguments); // 输出: [1, 2, 3]
})(1, 2);
Call
导演巧妙地让 Array.prototype.push
方法在非数组的对象上施展魔法,成功地添加了一个新成员。
无论是改变this
的指向,还是灵活地传递参数,甚至是借用其他对象的方法,Call
和 Apply
总能以其独特的技巧让每一个函数都成为舞台上的明星。
致敬—— 《JavaScript设计模式》· 曾探