阅读 129

少侠,这可能是关于 call 函数最好的文章~

image

阅读须知:这篇文章之前还有3篇相关的文章,如果少侠你没有阅读过的话,建议从第一篇开始看,不然可能会对某些地方比较困惑。

1、少侠,这可能是目前为止关于this最好的文章~

2、少侠,prototype前传,了解一下?

3、少侠, JS基础知识产生的蝴蝶效应能有多大?~


少侠们好~

又是一段时间没见了,

这次时间比较长,应该很想我吧,哈哈。

最近肺炎疫情严重,希望各位少侠都还安好,

记得老老实实待在家,不要出去玩。

上次结束前给大家留了两个未完成的问题,

image

少侠们都完成了没?

不过我估计,大多数少侠应该都是习惯性看完就关掉了,真正认真对待并动手的应该是极少数,另外还有少部分可能会等着这次看答案。

首先要给少侠们说一下,其实上次不是故意吊大家胃口,最后这两种情况,看着没有太大区别,但是却需要我们掌握一个重要的知识,也就是:

如果我有一个带有 this 函数,如何完全控制它被调用时的 this 指向?

image

少侠你可能会说,

天辰你最近都反复提到了,你将它放在哪个对象上调用,它里面的 this 就指向它,所以我们可以通过将它放在一个对象上调用。

image

比如这里的例子中,我们将 fn 放在 obj1 上,然后调用 ob1.fn ,得到的结果就是 obj1 中的 name,obj2 也是一样的道理。

初识 call 函数

为了方便,

我们可以先试着将这个过程单独放进一个函数里,

同时我们给它取一个名字,叫做 call :

image

这个函数接受一个对象,和一个fn 函数,它会将 fn 函数放置在 obj 上调用,所以可以实现 fn 中的 this 指向 obj 的效果,并且返回最终的结果 result。

下面是它的使用方式

image

不过,目前的 call 函数,实际上会有副作用,调用 call 函数后,obj 中会多了一个 fn 属性,也就是我们的 fn 函数,

而最开始的情况下,它是没有的。

image

所以,我们得想办法清除掉这个副作用,

在这里,一些少侠的第一直觉想法可能是,

完事后直接删除掉 obj 上的 fn 属性

image

如果少侠你是这样想的话,我只能说,

青涩!

如果 obj 上之前本身就有一个 fn 属性呢?

image

所以现在就比较尴尬,

不删除的话,我们的 call 函数会额外增加一个 fn 属性,删除的话,又可能删除掉原先对象上本身就存在的 fn 属性。

怎么办呢?

有些少侠可能会说,

那能不能先检测一下,如果obj上有一个 fn 函数,我们就临时放在另外一个属性上,比如 fn2呢?

当然可以,

前提是你也必须保证 obj1 上没有一个叫做 fn2 的属性,不然你可能又得尝试 fn3,fn4, fn5。。。

看着就感觉很 low !

“那我不能找个稀有的属性名称吗? 比如用一个很长的不容易冲突的字符串,比如 UUID。”

“对,或者就放 '天辰dreamer' 属性上,除了你,应该没有人会取这么奇怪的名称!”

image

呵呵,我代码中的变量名全是 XXXdreamer 的格式难道我会告诉你?

而类似 UUID 这种很长的随机字符串的话,

按照道理来说几乎不可能冲突,但是,

万一少侠你是万中无一的随机天才呢?

image

所以!

更好的办法是什么呢?

更好的办法是将 obj 上原来的 fn 属性用一个变量保存好,过后再放回去:

image

这里,我们利用了一个 savedFn 变量先缓存之前的 obj.fn,过后再根据 savedFn 是否存在决定是否要重新放回去。

image

好了,现在我们的 call 函数比之前好多了,

但是,等等!!!

如果我们的 fn 函数还需要参数怎么办?

image

所以我们需要一种方式来通过 call 函数将参数传递到 fn 中。

比如,给 call 函数增加一个额外的参数:

image

好了,

这样是可以的,

我们可以通过给 call 增加额外的参数来传递到 fn 内部,

但是!问题又来了,

这里我们只增加了一个固定参数,

当我们遇见其他函数时,它可能需要接受多个参数。

我们如何知道 fn 函数会接受多少参数呢?

这个问题我们可以先使用 ES6 的新语法,

展开运算符:

image

这样的话,我们的 call 函数就基本成型了,

我们来试一试:

image

少侠你可以看到,我们的 addTitle 函数接受一个前缀和一个后缀,然后我们通过 call 函数调用 将其中的 this 指向 user,并传递一个 🍄 当做前缀,一个 'dreamer' 当做后缀, 然后返回最终的字符串 "🍄天辰dreamer"

完全ok~

少侠你可能会猜到我要说什么了。

没错!

你也不需要手动实现 call 函数, JS 中有一个现成的 call 函数,

点击这里查看JS中的call

所有的函数默认都有一个叫做 call 的方法,你可以直接使用它:

image

注意调用方式的区别:

image

好了,少侠你已经知道 JS 中的 call 函数是怎么一回事了,也知道了如何手动模拟一个类似的 call 函数。

一般来说,文章到这里应该就要结束了,

但是,我如果到这里就结束了,有点对不起我的标题!

因为我们确实还有一些问题没有考虑到,

比如:

展开运算符是 ES6 的语法,而系统自带的 call 函数是 ES5 的语法,能不能不用ES6 的语法实现它呢?

没错,展开运算符是 ES6 的语法,它时间上比 call 函数出现得更晚,也就是说,如果都能使用展开运算符的话,我们也应该直接可以使用 call 函数。

所以用 ES6 的语法实现 ES5 的语法等于是使用了未来的技术。

那么不用展开运算符怎么解决参数不确定问题呢?

其实也不是很难,

比如。。。。

少侠你手动多写一些参数。。。。

写10个参数,应该够用了吧?

image

哈哈哈哈,没想到吧!

如果你问我遇见 10 个以上参数的函数怎么办?

100 个?

如果这样的话,

对不起,这么 low 的函数不配使用我的 call 函数!

“你自己的 call 函数还使用了 12 个参数呢!”

“就你话多!”

当然,实际上少侠你也可以从另外的层面解决问题,

比如如果函数是少侠你自己写的话,你可以选择所有函数都只采用一个对象来接受所有参数。

这样就只需要一个额外参数了,而且更方便,可读性也更高。

image

或者,如果实在要实现任意参数的话,

可以选择使用 eval 来动态解析代码:

JS中的eval

image

这个函数要稍微复杂一些,

而且可能也有一些需要少侠你额外了解的内容,

比如在函数内部可以通过 arguments[0] 获取第一个传递进来的参数,arguments[1] 获取第二个参数等等, 数组 [1,2] 转化成字符串后是 '1,2',以及 eval 可以动态解析代码等等。

如果少侠你不是很清楚的话,可以花一些时间额外了解一下。

而且,少侠你可能也听过,永远不要使用 eval 这句话,因为在 JS 中 使用 eval 是比较危险的事,而且性能也比较低。

所以, 10 个参数的函数其实也不是很坏,对吧?

好了,

在最后的 call 函数中,我们没有使用任何 es6 新语法,全部换成了老的语法,同时也解决了不定参数的问题。

那么,还有漏掉的地方吗?

比如,

我们目前实现的 call 函数,和系统自带的 call 函数,除了使用方式不一样之外,还有什么区别没有?

能直接替代它吗?

不完美的 call 函数?

为了弄清楚这个问题,

我们先对比看看我们的 call 函数和系统内置的 call 函数:

* 内置的 call 函数 我们的 call 函数
调用方式 对象方法方式调用 普通函数方式调用
会产生副作用吗 不会,不会无意中删除或覆盖obj对象原有属性 不会,不会无意中删除或覆盖obj对象原有属性
能不能接受任意参数 可以接受任意参数 可以实现接受任意参数

主要就上面几个关键的点,

少侠你可以看到,除了调用方式稍微有点区别以外,似乎没有区别了。

到这里,

少侠你也许认为我们的 call 函数和系统内置的 call 函数功能已经一模一样了。

实际上,

上面最后的 call 函数,确实还算比较完善,

如果少侠你网上搜一下其他关于 call 函数的实现,可能还不一定比我们这里实现的更好。

“装什么逼呢,我马上就去搜一下。”

“我搜了一下,果然是这样,天辰你真牛逼!”

------ 一位口口声声说不是天辰小号的人说道。

好了,还是继续认真说 call 函数的事。

真相是,我们上面最后的 call 函数,看似没有什么问题了,但是在一些少见的边缘情况,还是会有问题。

主要是我们实现方式中的这一行代码:

image

这里我们将 fn 函数赋值给了 obj 中,作为一个 fn 属性。

有什么问题呢?

问题就是,

事实上我们不一定总是能够成功将 fn 函数赋值给 obj.fn 属性,也就是说,

有些情况下,给一个对象属性赋值可能会失败

比如,如果一个对象是冻结后的对象的话,我们是不能给它添加任何属性的,也不能改变它的任何属性

image

点击查看JS中的Object.freeze()

这个时候,如果使用我们的 call 函数的话,就会出错:

image
原因就是,

我们 call 函数内部试图向 obj 上添加 fn 属性,但是对于一个冻结对象来说,这个过程会静默的失败,所以最后调用 obj.fn 时就会出错,

但是使用系统内置的 call 函数却不会有这个问题:

image

很可惜吧?

好像就差一点。

少侠你可能会想,有没有什么办法,让我们的 call 函数,也能够解决这个问题呢?

毕竟,我们一步一步走了这么远,

有些事,在最后时刻放弃,可能比一开始就放弃更加遗憾。

如果少侠你生活中有这样的遗憾的话,

那么在 JS 的世界里,

是否会有这么一丝曙光,

让我们的 call 函数不再遗憾呢?

这个问题暂时留给少侠你当做练习题~

在下一次的故事中,我们再一起想办法解决这个问题~

好了,

少侠,

江湖路远,有缘再见!


试炼山谷

如果少侠你看到了这里,

你应该也知道了,目前我们的 call 函数比起系统的 call 函数还是有一些差距的,

就能不能解决特殊对象比如冻结对象的话题,

下面有 4 个 dreamer 发表了他们的看法,

少侠你认为,哪个会是最终的正确答案呢?

“我认为可以,但是也许需要使用到我们目前还没遇见的技能,比如 prototype。”

———— 乌云dreamer

“嗯,应该可以,但也许我们之前遇见的技能以及足够解决它了,比如 mixin。”

———— 凉风dreamer

“嗯。。。不确定,不过看起来好像不可以呢。”

———— 秋月dreamer

“换其他人应该不可以,但是天辰dreamer一定可以!”

———— 不是天辰dreamer


小酒馆

实际上如果可以使用未来语法的话,我们也可以使用 Symbol 来保证对象属性名称不冲突,

但是在最后我们已经不使用未来语法了,所以也就不考虑这种方式了~

一些你可能关心的问题

1、为什么可以用 savedFn 保存之前的 obj.fn? 在后面给 obj.fn 重新赋值时,savedFn 不会改变吗?

我想少侠你困惑的地方可能是这种操作:

image

正确答案是会返回一个土豆,

如果你不知道为什么的话,

请重新复习一下上一节的内容!

少侠, JS基础知识产生的蝴蝶效应能有多大?~

2、为什么代码要用图片呢?

这个问题我隔一段时间要回答一次。。。

首先,我认为我的图片要好看一些,

然后就是如果少侠你是手机访问的话,直接放大查看图片比手动左右滑来滑去看代码要方便一些。

最后就是如果你想测试的话,亲自动手敲一遍代码比单纯地复制粘贴也要好一些!

一箭三雕!

3、上次留的坑还没解释呢,结果这次结尾又留一个坑?

哈哈,上次留的坑还没到填的时候,这次留坑是因为剩余内容加上的话,文章会过于长了,所以下次单独用一篇来说。

我保证,下次,下次一定,一定先把这次的坑先填了!


声明:本文仅限于潇洒有趣又很酷的天辰dreamer装逼使用,转载请注明原作者和出处,商业转载请联系我(如果真有的话)。。。

image

关注下面的标签,发现更多相似文章
评论