深入理解JavaScript设计模式之call,apply,this

发布于:2025-05-29 ⋅ 阅读:(24) ⋅ 点赞:(0)

前言:

最近在看曾探老师的《JavaScript设计模式》,说实话,可能是我基础还不够扎实,书里的很多内容对我来说不是“一看就懂”,而是“看了三遍才懂一点点”。特别是像代码逻辑、概念解释这些地方,常常是一次看不懂,就来两次;两次还迷糊,就再来一次。但我慢慢也想通了,学习本来就是一个不断重复的过程。每一次重读,都能从字里行间中发现新的理解,就像挖宝藏一样,每次翻一翻都有新收获。为了让自己记得更牢,也为了让以后复习的时候不那么枯燥,我在反复阅读第二章“this、call、apply”之后,试着用自己的话,把知识点编成了一个小故事的形式,讲给自己听。
我想,这不仅是一种学习方式,也是我对这本书的一种致敬。

this找家

想象一下,this就像是一个四处旅行的人,他总在寻找它的“家”,但是问题来了,这个人(this)的家并不固定,取决于他是怎么被邀请这个地方的,在javaScript中,this就是一个旅行者,他的指向完全依赖于函数的调用方式。

  1. 如果是对象的方法调用:比如obj.myMethod(),那么this的家就安在obj里面。
  2. 如果是作为构造函数调用:比如new MyClass(),这时候this就有了自己的新家,一个新的对象实例
  3. 如果是直接调用函数,比如myFunction(),那么严格模式下,this就会变得无家可归,即undefined,而在非严格模式下,则默认回到了全局对象浏览器的window的怀抱。

call 和 apply 导游

现在,比如我们的this旅行者到了一个陌生的城市,不知道如何找到正确的家,这时候就可以请callapply两位热心的导游出现了。
call这个导游很细心,他会亲自带着this去正确的地方,并且还允许你指定额外的参数列表,就像myFunction.call(context,arg1,arg2)。在这里,context就是我们希望this去的家,而后面参数就是这次旅行所需要的装备。
applycall不同的是,apply更喜欢一次性把所有的东西都准备好再出发,所以除了第一个参数用来指定this应该去的"家"外,它接收第二个参数为一个数组或类数组对象,里面包含了所需要传递给函数的参数,如myFunction.apply(context,[arg1,arg2]),总之,当你想要控制this的指向,或者需要特定的方式传递参数的时候,callapply就是你的得力导游,他们不仅能帮this找到回家的路,还能确保旅途愉快顺利。

this的指向

第一站、作为对象的方法被调用(this回到家)

有一天,this被一个叫做obj的大叔收留了,成为了大叔家的佣人。

var obj = {
    a: 1,
    getA: function() {
        console.log(this === obj); // true
        console.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>

深入理解JavaScript设计模式之call,apply,this的深入理解
终于通过导演的不懈努力与分析让this回到了正确的家庭总结了整个剧组,甚至还总结了口诀:

this是谁调用了我,我就跟谁混。
call/applythis的导演,想让this演谁都可以。

发生了上面的事情之后,this学会了如何在javaScript世界中找到自己的归属,它不再轻易迷路,也不再害怕被借来借去,从此要记住:

  1. 多问问this:“whu ar you ?" 你是谁?
  2. 多请callapply来帮忙。
  3. 别忘了用bindthat=thisthis上个保险!

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的参数在内部就用一个数组来表示的,从这个意义上说,applycall使用率更高,不必关心具体多少个参数被传入函数,只要作用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); // 输出:sven
    alert([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

通过callapply大导演的帮助下,getName终于找到了它真正的家!

借用其他对象的方法:杜鹃鸟的智慧

JavaScript中也有一种"借壳生蛋"的艺术,就像杜鹃鸟将自己的蛋托付给其他鸟类孵化一样,这里callapply也能帮我们借用其他对象的方法来完成一些任务。
例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的指向,还是灵活地传递参数,甚至是借用其他对象的方法,CallApply 总能以其独特的技巧让每一个函数都成为舞台上的明星。

致敬—— 《JavaScript设计模式》· 曾探

完~


网站公告

今日签到

点亮在社区的每一天
去签到