Open jawil opened 7 years ago
好巧,我们今天的文章同时讲到了call和apply的模拟实现
不过现在的bind的模拟实现其实是有个小问题的,我专门私信了颜海镜求证了这个问题。
length问题?@mqyqingfeng 同事朋友去51信用卡遇到这个面试题,我上周写了一半,今天看见你发了,我也赶紧写完,发现思路都差不多,我主要是先实现apply引出的。。。😄
并不是哦,如果你比较MDN英文版和中文版的实现,就会发现这个问题。先卖个关子,明天发布的文章讲解bind的模拟实现就会讲到这个问题。(๑•̀ㅂ•́)و✧
恩恩,我会关注呢,反正你一发就会有邮件推送😄
个人优化后的版本
Function.prototype.applyOne = function(context){
context.fn = this;
eval('context.fn('+(arguments[1]||[]).toString()+')');
delete context.fn;
}
(我也算参与过1.2k的大项目了么
仔细看了你的这篇文章,嗯,觉得……哈哈,建议楼主在整理资料的过程中慢慢找到自己的思路去表达,否则按照参考文章的思路去写,容易显得似曾相识。我自己也有这样的问题,与楼主共勉。
看了有些文章,最后那个三元判断是这种的 this instanceof F ? this : context || window 有点迷糊
在非严格模式下,走后面逻辑的话此时的this就是指向window,其实是一个东西。。。@xumeiyan
sayHello.applyFive(obj,['hello']是有问题的,eval解析时hello未定义,用toString也是不行的
Function.prototype.applyTwo = function(context) {
var argsPar=arguments[1]
context.fn = this;
var args = [];
for (var i = 0, len = argsPar.length; i < len; i++) {
args.push('argsPar[' + i + ']');
}
var str='context.fn(' + args + ')'
eval('context.fn(' + args + ')');
delete context.fn; //执行完毕之后删除这个属性
}
//测试一下
var jawil = {
name: "jawil",
sayHello: function(age) {
console.log(this.name, age);
}
};
var lulin = {
name: "lulin",
};
jawil.sayHello.applyTwo(lulin, ['hello']) //lulin 24
改成这样就ok了@Mrzhangyasheng
都是基础啊,理解原理了自然就可以写出来了
大多数(可能是所有)原生对象上的方法的prototype
为undefined
, 比如 Array.prototype.push.prototype === undefined
. 于是这样使用会报错:
var obj = {0: 'a', 1: 'b', length: 2}
// 原生bind
var each1 = Array.prototype.forEach.bind(obj, function(item) {
console.log(item)
})
var each2 = Array.prototype.forEach.bindOne(obj, function (item) {
console.log(item)
})
each1() // a b
each2() // Uncaught TypeError: Function has non-object prototype 'undefined' in instanceof check
个人做了这样的优化
var F = function () {};
F.prototype = this.prototype || Object.create(null);
感谢补充优化,不过Object.create(null)好像是ES5的方法,用在这里hack好像不太合适,不过也没事,其实Object.create的也很好模拟实现 @leat14536
请教一个问题,您实现apply的第三步中通过以下方式来实现当this
为空时指向window
:
var context = context || window
可是,当this
为空时,apply
应该是不能传递参数的吧?
applyFive([1,2])
这样是不行的
这块我有点不明白,请您指教一下。。
@axuebin 处理的是这种情况 fn.apply(null, [1, 2])
感谢@mqyqingfeng 百忙之中作出回答😂
明白了。。我之前有一个地方写错了,导致apply()
即没this又没参数时出错了。。谢谢~
不错不错,厉害,不过applyFive没传参的直接执行的时候没有执行delete context[fn]。
if (args == void 0) { //没有传入参数直接执行
var returnValue = context[fn]();
delete context[fn];
return returnValue;
}
博主真厉害,按照您的步骤进行到如图标红的一步,发现args 是上图中的第一行代码(是个对象),但是却有个length属性,能不能解释一下为什么啊?
@gaopeng0108 arguments 对象是一个类数组对象,就是会有一个 length 属性,你在浏览器中可以看到这个属性,可以参考这篇文章 https://github.com/mqyqingfeng/Blog/issues/14
原生的apply 支持 参数是对象 如果用eval模拟要完全用字符串拼接 不然会有问题
@jawil 应该吧
var fnStr = 'context[fn]('
for (var i = 0; i < args.length; i++) {
//得到"context.fn(arg1,arg2,arg3...)"这个字符串在,最后用eval执行
fnStr += i == args.length - 1 ? args[i] : args[i] + ','
}
fnStr += ')'
改为
var fnStr = 'context[fn]('
for (var i = 0; i < args.length; i++) {
//得到"context.fn(arg1,arg2,arg3...)"这个字符串在,最后用eval执行
fnStr += i == args.length - 1 ? 'args[' + i + ']' : 'args[' + i + ']' + ','
}
fnStr += ')'
当初考虑的情况确实只是最简单的字符串,因为只是提供一种实现的思维,感谢指正 @zyg-github
飚一句zanghua,前端感觉真TM任重而道远
apply在没有传参的时候直接返回了context[fn](),但是好像没有把context的fn删除呢
厉害!
莫非你就是1024康先生,麻烦送一码。1024。
哈哈哈哈哈,我前几天在某个群里也看到这条题目,我今天想了半天,偶然意识到可以这样:
// ...省略
obj.fn = fn
let result = obj.fn(...arr)
delete obj.fn
return result
我写完觉得这样是不是不好,然后我就网上搜,发现你大致也是这样做的,开心~
这是来自QQ邮箱的自动回复邮件。 您好,您的邮件我已收到,谢谢!
我是黄海川。你的邮件我已经收到,谢谢!
bind方法传递给调用函数的参数可以逐个列出,也可以写在数组中。
这句感觉不太对吧,需要逐个列出,不可以放在数组中
我是黄海川。你的邮件我已经收到,谢谢!
本文首发我的个人博客:前端小密圈,评论交流送1024邀请码,嘿嘿嘿😄。
来自朋友去某信用卡管家的做的一道面试题,用原生
JavaScript
模拟ES5
的bind
方法,不准用call
和bind
方法。至于结果嘛。。。那个人当然是没写出来,我就自己尝试研究了一番,其实早就写了,一直没有组织好语言发出来。
额。。。这个题有点刁钻,这是对
JavaScript
基本功很好的一个检测,看你JavaScript
掌握的怎么样以及平时有没有去深入研究一些方法的实现,简而言之,就是有没有折腾精神。不准用不用
call
和apply
方法,这个没啥好说的,不准用我们就用原生JavaScript
先来模拟一个apply
方法,感兴趣的童鞋也可以看看chrome
的v8
怎么实现这个方法的,这里我只按照自己的思维实现,在模拟之前我们先要明白和了解原生call
和apply
方法是什么。简单粗暴地来说,
call
,apply
,bind
是用于绑定this
指向的。(如果你还不了解JS中this的指向问题,以及执行环境上下文的奥秘,这篇文章暂时就不太适合阅读)。什么是call和apply方法
我们单独看看
ECMAScript
规范对apply
的定义,看个大概就行:15.3.4.3 Function.prototype.apply (thisArg, argArray)
顺便贴一贴中文版,免得翻译一下,中文版地址:
通过定义简单说一下call和apply方法,他们就是参数不同,作用基本相同。
知道定义然后,直接看个简单的demo
然后看看使用
apply
和call
之后的输出:结果都相同。从写法上我们就能看出二者之间的异同。相同之处在于,第一个参数都是要绑定的上下文,后面的参数是要传递给调用该方法的函数的。不同之处在于,call方法传递给调用函数的参数是逐个列出的,而apply则是要写在数组中。
总结一句话介绍
call
和apply
分析call和apply的原理
上面代码,我们注意到了两点:
call
和apply
改变了this
的指向,指向到lulin
sayHello
函数执行了这里默认大家都对
this
有一个基本的了解,知道什么时候this
该指向谁,我们结合这两句话来分析这个通用函数:f.apply(o)
,我们直接看一本书对其中原理的解读,具体什么书,我也不知道,参数我们先不管,先了解其中的大致原理。注意红色框中的部分,f.call(o)其原理就是先通过 o.m = f 将 f作为o的某个临时属性m存储,然后执行m,执行完毕后将m属性删除。
知道了这个基本原来我们再来看看刚才
jawil.sayHello.call(lulin, 24)
执行的过程:上面的说的是原理,可能你看的还有点抽象,下面我们用代码模拟实现
apply
一下。实现apply方法
模拟实现第一步
根据这个思路,我们可以尝试着去写第一版的 applyOne 函数:
正好可以打印lulin而不是之前的jawil了,哎,不容易啊!😄
模拟实现第二步
最一开始也讲了,
apply
函数还能给定参数执行函数。举个例子:注意:传入的参数就是一个数组,很简单,我们可以从
Arguments
对象中取值,Arguments
不知道是何物,赶紧补习,此文也不太适合初学者,第二个参数就是数组对象,但是执行的时候要把数组数值传递给函数当参数,然后执行,这就需要一点小技巧。参数问题其实很简单,我们先偷个懒,我们接着要把这个参数数组放到要执行的函数的参数里面去。
很简单是不是,那你就错了,数组join方法返回的是啥?
typeof [1,2,3,4].join(',')//string
Too young,too simple啊,最后是一个 "1,2,3,4" 的字符串,其实就是一个参数,肯定不行啦。
也许有人会想到用ES6的一些奇淫方法,不过
apply
是ES3
的方法,我们为了模拟实现一个ES3
的方法,要用到ES6
的方法,反正面试官也没说不准这样。但是我们这次用eval
方法拼成一个函数,类似于这样:eval('context.fn(' + args +')')
先简单了解一下eval函数吧 定义和用法
语法:
eval(string)
简单来说吧,就是用JavaScript的解析引擎来解析这一堆字符串里面的内容,这么说吧,你可以这么理解,你把
eval
看成是<script>
标签。eval('function Test(a,b,c,d){console.log(a,b,c,d)};Test(1,2,3,4)')
就是相当于这样
第二版代码大致如下:
好像就行了是不是,其实这只是最粗糙的版本,能用,但是不完善,完成了大约百分之六七十了。
模拟实现第三步
其实还有几个小地方需要注意:
1.
this
参数可以传null
或者不传,当为null
的时候,视为指向window
举个两个简单栗子栗子🌰: demo1:
demo2:
2.函数是可以有返回值的.
举个简单栗子🌰:
这些都是小问题,想到了,就很好解决。我们来看看此时的第三版
apply
模拟方法。好紧张,再来做个小测试,demo,应该不会出问题:
完美?perfact?这就好了,不存在的,我们来看看第四步的实现。
模拟实现第四步
其实一开始就埋下了一个隐患,我们看看这段代码:
就是这句话,
context.fn = this //假想context对象预先不存在名为fn的属性
,这就是一开始的隐患,我们只是假设,但是并不能防止contenx
对象一开始就没有这个属性,要想做到完美,就要保证这个context.fn
中的fn
的唯一性。于是我自然而然的想到了强大的
ES6
,这玩意还是好用啊,幸好早就了解并一直在使用ES6
,还没有学习过ES6的童鞋赶紧学习一下,没有坏处的。重新复习下新知识: 基本数据类型有6种:
Undefined
、Null
、布尔值(Boolean)
、字符串(String)
、数值(Number)
、对象(Object)
。ES5对象属性名都是字符串容易造成属性名的冲突。 举个栗子🌰:
ES6
引入了一种新的原始数据类型Symbol
,表示独一无二的值。注意,
Symbol
函数前不能使用new
命令,否则会报错。这是因为生成的Symbol
是一个原始类型的值,不是对象Symbol
函数可以接受一个字符串作为参数,表示对Symbol
实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。作为属性名的Symbol
看看下面这个栗子🌰:
Symbol
值作为属性名时,该属性还是公开属性,不是私有属性。这个有点类似于
java
中的protected
属性(protected和private的区别:在类的外部都是不可以访问的,在类内的子类可以继承protected不可以继承private)但是这里的Symbol在类外部也是可以访问的,只是不会出现在
for...in
、for...of
循环中,也不会被Object.keys()
、Object.getOwnPropertyNames()
返回。但有一个Object.getOwnPropertySymbols
方法,可以获取指定对象的所有Symbol
属性名。看看第四版的实现demo,想必大家了解上面知识已经猜得到怎么写了,很简单。 直接加个
var fn = Symbol()
就行了,,,模拟实现第五步
呃呃呃额额,慢着,
ES3
就出现的方法,你用ES6
来实现,你好意思么?你可能会说,不管黑猫白猫,只要能抓住老鼠的猫就是好猫,面试官直说不准用call
和apply
方法但是没说不准用ES6
语法啊。反正公说公有理婆说婆有理,这里还是不用
Symbol
方法实现一下,我们知道,ES6其实都是语法糖,ES6
能写的,咋们ES5
都能实现,这就导致了babel
这类把ES6
语法转化成ES5
的代码了。至于
babel
把Symbol
属性转换成啥代码了,我也没去看,有兴趣的可以看一下稍微研究一下,这里我说一下简单的模拟。ES5
没有Sybmol
,属性名称只可能是一个字符串,如果我们能做到这个字符串不可预料,那么就基本达到目标。要达到不可预期,一个随机数基本上就解决了。好紧张,再来做个小测试,demo,应该不会出问题:
到此,我们完成了apply的模拟实现,给自己一个赞 b( ̄▽ ̄)d
实现Call方法
这个不需要讲了吧,道理都一样,就是参数一样,这里我给出我实现的一种方式,看不懂,自己写一个去。
看不太明白也不能怪我咯,我就不细讲了,看个demo证明一下,这个写法没问题。
实现bind方法
养兵千日,用兵一时。
什么是bind函数
如果掌握了上面实现
apply
的方法,我想理解起来模拟实现bind
方法也是轻而易举,原理都差不多,我们还是来看看bind
方法的定义。我们还是简单的看下
ECMAScript
规范对bind
方法的定义,暂时看不懂不要紧,获取几个关键信息就行。15.3.4.5 Function.prototype.bind (thisArg [, arg1 [, arg2, …]])
注意一点,ECMAScript规范提到: Function.prototype.bind 创建的函数对象不包含 prototype 属性或 [[Code]], [[FormalParameters]], [[Scope]] 内部属性。
语法是这样样子的:
fun.bind(thisArg[, arg1[, arg2[, ...]]])
呃呃呃,是不是似曾相识,这不是call方法的语法一个样子么,,,但它们是一样的吗?
bind方法传递给调用函数的参数可以逐个列出,也可以写在数组中。bind方法与call、apply最大的不同就是前者返回一个绑定上下文的函数,而后两者是直接执行了函数。由于这个原因,上面的代码也可以这样写:
bind方法还可以这样写
fn.bind(obj, arg1)(arg2)
.用一句话总结bind的用法:该方法创建一个新函数,称为绑定函数,绑定函数会以创建它时传入bind方法的第一个参数作为this,传入bind方法的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数。
bind在实际中的应用
实际使用中我们经常会碰到这样的问题:
这个时候输出的this.nickname是undefined,原因是this指向是在运行函数时确定的,而不是定义函数时候确定的,再因为setTimeout在全局环境下执行,所以this指向setTimeout的上下文:window。关于this指向问题,这里就不细扯
以前解决这个问题的办法通常是缓存
this
,例如:这样就解决了这个问题,非常方便,因为它使得setTimeout函数中可以访问Person的上下文。但是看起来稍微一种蛋蛋的忧伤。
但是现在有一个更好的办法!您可以使用
bind
。上面的例子中被更新为:bind() 最简单的用法是创建一个函数,使这个函数不论怎么调用都有同样的 this 值。JavaScript新手经常犯的一个错误是将一个方法从对象中拿出来,然后再调用,希望方法中的 this 是原来的对象。(比如在回调中传入这个方法。)如果不做特殊处理的话,一般会丢失原来的对象。从原来的函数和原来的对象创建一个绑定函数,则能很漂亮地解决这个问题:
很不幸,Function.prototype.bind 在IE8及以下的版本中不被支持,所以如果你没有一个备用方案的话,可能在运行时会出现问题。bind 函数在 ECMA-262 第五版才被加入;它可能无法在所有浏览器上运行。你可以部份地在脚本开头加入以下代码,就能使它运作,让不支持的浏览器也能使用 bind() 功能。
幸运的是,我们可以自己来模拟
bind
功能:初级实现
了解了以上内容,我们来实现一个初级的
bind
函数Polyfill
:我们先简要解读一下: 基本原理是使用
apply
进行模拟。函数体内的this
,就是需要绑定this
的实例函数,或者说是原函数。最后我们使用apply
来进行参数(context)绑定,并返回。 同时,将第一个参数(context)以外的其他参数,作为提供给原函数的预设参数,这也是基本的“颗粒化(curring)”基础。初级实现的加分项
上面的实现(包括后面的实现),其实是一个典型的“Monkey patching(猴子补丁)”,即“给内置对象扩展方法”。所以,如果面试者能进行一下“嗅探”,进行兼容处理,就是锦上添花了。
颗粒化(curring)实现
对于函数的柯里化不太了解的童鞋,可以先尝试读读这篇文章:前端基础进阶(八):深入详解函数的柯里化。 上述的实现方式中,我们返回的参数列表里包含:
atgsArray.slice(1)
,他的问题在于存在预置参数功能丢失的现象。 想象我们返回的绑定函数中,如果想实现预设传参(就像bind
所实现的那样),就面临尴尬的局面。真正实现颗粒化的“完美方式”是:上面什么是bind函数还介绍到:
bind返回的函数如果作为构造函数,搭配new关键字出现的话,我们的绑定this就需要“被忽略”。
构造函数场景下的兼容
有了上边的讲解,不难理解需要兼容构造函数场景的实现:
更严谨的做法
我们需要调用
bind
方法的一定要是一个函数,所以可以在函数体内做一个判断:做到所有这一切,基本算是完成了。其实MDN上有个自己实现的polyfill,就是如此实现的。 另外,《JavaScript Web Application》一书中对bind()的实现,也是如此。
最终答案
好紧张,最后来做个小测试,demo,应该不会出问题:
看了这篇文章,以后再遇到类似的问题,应该能够顺利通过吧~
参考文章
ES6入门之Symbol ECMAScript 5.1(英文版) 从一道面试题,到“我可能看了假源码”