本文首发个人我的博客:前端小密圈,评论交流送1024邀请码,嘿嘿嘿?。html
来自朋友去某信用卡管家的作的一道面试题,用原生JavaScript
模拟ES5
的bind
方法,不许用call
和bind
方法。前端
至于结果嘛。。。那我的固然是没写出来,我就本身尝试研究了一番,其实早就写了,一直没有组织好语言发出来。java
额。。。这个题有点刁钻,这是对JavaScript
基本功很好的一个检测,看你JavaScript
掌握的怎么样以及平时有没有去深刻研究一些方法的实现,简而言之,就是有没有折腾精神。git
不许用不用call
和apply
方法,这个没啥好说的,不许用咱们就用原生JavaScript
先来模拟一个apply
方法,感兴趣的童鞋也能够看看chrome
的v8
怎么实现这个方法的,这里我只按照本身的思惟实现,在模拟以前咱们先要明白和了解原生call
和apply
方法是什么。github
简单粗暴地来讲,call
,apply
,bind
是用于绑定this
指向的。(若是你还不了解JS中this的指向问题,以及执行环境上下文的奥秘,这篇文章暂时就不太适合阅读)。面试
咱们单独看看ECMAScript
规范对apply
的定义,看个大概就行:chrome
15.3.4.3 Function.prototype.apply (thisArg, argArray) 数组
顺便贴一贴中文版,省得翻译一下,中文版地址:浏览器
经过定义简单说一下call和apply方法,他们就是参数不一样,做用基本相同。缓存
一、每一个函数都包含两个非继承而来的方法:apply()和call()。
二、他们的用途相同,都是在特定的做用域中调用函数。
三、接收参数方面不一样,apply()接收两个参数,一个是函数运行的做用域(this),另外一个是参数数组。
四、call()方法第一个参数与apply()方法相同,但传递给函数的参数必须列举出来。
知道定义而后,直接看个简单的demo
var jawil = { name: "jawil", sayHello: function (age) { console.log("hello, i am ", this.name + " " + age + " years old"); } }; var lulin = { name: "lulin", }; jawil.sayHello(24); // hello, i am jawil 24 years old
而后看看使用apply
和call
以后的输出:
jawil.sayHello.call(lulin, 24);// hello, i am lulin 24 years old jawil.sayHello.apply(lulin, [24]);// hello, i am lulin 24 years old
结果都相同。从写法上咱们就能看出两者之间的异同。相同之处在于,第一个参数都是要绑定的上下文,后面的参数是要传递给调用该方法的函数的。不一样之处在于,call方法传递给调用函数的参数是逐个列出的,而apply则是要写在数组中。
总结一句话介绍call
和apply
call()
方法在使用一个指定的this
值和若干个指定的参数值的前提下调用某个函数或方法。apply()
方法在使用一个指定的this
值和参数值必须是数组类型的前提下调用某个函数或方法。
上面代码,咱们注意到了两点:
call
和apply
改变了this
的指向,指向到lulin
sayHello
函数执行了
这里默认你们都对this
有一个基本的了解,知道何时this
该指向谁,咱们结合这两句话来分析这个通用函数:f.apply(o)
,咱们直接看一本书对其中原理的解读,具体什么书,我也不知道,参数咱们先无论,先了解其中的大体原理。
<img src="http://images2015.cnblogs.com...
注意红色框中的部分,f.call(o)其原理就是先经过 o.m = f 将 f做为o的某个临时属性m存储,而后执行m,执行完毕后将m属性删除。
知道了这个基本原来咱们再来看看刚才jawil.sayHello.call(lulin, 24)
执行的过程:
// 第一步 lulin.fn = jawil.sayHello // 第二步 lulin.fn() // 第三步 delete lulin.fn
上面的说的是原理,可能你看的还有点抽象,下面咱们用代码模拟实现apply
一下。
根据这个思路,咱们能够尝试着去写初版的 applyOne 函数:
// 初版 Function.prototype.applyOne = function(context) { // 首先要获取调用call的函数,用this能够获取 context.fn = this; context.fn(); delete context.fn; } //简单写一个不带参数的demo var jawil = { name: "jawil", sayHello: function (age) { console.log(this.name); } }; var lulin = { name: "lulin", }; //看看结果: jawil.sayHello.applyOne(lulin)//lulin
正好能够打印lulin而不是以前的jawil了,哎,不容易啊!?
最一开始也讲了,apply
函数还能给定参数执行函数。举个例子:
var jawil = { name: "jawil", sayHello: function (age) { console.log(this.name,age); } }; var lulin = { name: "lulin", }; jawil.sayHello.apply(lulin,[24])//lulin 24
注意:传入的参数就是一个数组,很简单,咱们能够从Arguments
对象中取值,Arguments
不知道是何物,赶忙补习,此文也不太适合初学者,第二个参数就是数组对象,可是执行的时候要把数组数值传递给函数当参数,而后执行,这就须要一点小技巧。
参数问题其实很简单,咱们先偷个懒,咱们接着要把这个参数数组放到要执行的函数的参数里面去。
Function.prototype.applyTwo = function(context) { // 首先要获取调用call的函数,用this能够获取 context.fn = this; var args = arguments[1] //获取传入的数组参数 context.fn(args.join(','); delete context.fn; }
很简单是否是,那你就错了,数组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() 函数可计算某个字符串,并执行其中的的 JavaScript 代码。
语法:eval(string)
string必需。要计算的字符串,其中含有要计算的 JavaScript 表达式或要执行的语句。该方法只接受原始字符串做为参数,若是 string 参数不是原始字符串,那么该方法将不做任何改变地返回。所以请不要为 eval() 函数传递 String 对象来做为参数。
简单来讲吧,就是用JavaScript的解析引擎来解析这一堆字符串里面的内容,这么说吧,你能够这么理解,你把eval
当作是<script>
标签。
eval('function Test(a,b,c,d){console.log(a,b,c,d)};Test(1,2,3,4)')
就是至关于这样
<script> function Test(a,b,c,d){ console.log(a,b,c,d) }; Test(1,2,3,4) </script>
第二版代码大体以下:
Function.prototype.applyTwo = function(context) { var args = arguments[1]; //获取传入的数组参数 context.fn = this; //假想context对象预先不存在名为fn的属性 var fnStr = 'context.fn('; for (var i = 0; i < args.length; i++) { fnStr += i == args.length - 1 ? args[i] : args[i] + ','; } fnStr += ')';//获得"context.fn(arg1,arg2,arg3...)"这个字符串在,最后用eval执行 eval(fnStr); //仍是eval强大 delete context.fn; //执行完毕以后删除这个属性 } //测试一下 var jawil = { name: "jawil", sayHello: function (age) { console.log(this.name,age); } }; var lulin = { name: "lulin", }; jawil.sayHello.applyTwo(lulin,[24])//lulin 24
好像就好了是否是,其实这只是最粗糙的版本,能用,可是不完善,完成了大约百分之六七十了。
其实还有几个小地方须要注意:
1.this
参数能够传null
或者不传,当为null
的时候,视为指向window
举个两个简单栗子栗子?:
demo1:
var name = 'jawil'; function sayHello() { console.log(this.name); } sayHello.apply(null); // 'jawil'
demo2:
var name = 'jawil'; function sayHello() { console.log(this.name); } sayHello.apply(); // 'jawil'
2.函数是能够有返回值的.
举个简单栗子?:
var obj = { name: 'jawil' } function sayHello(age) { return { name: this.name, age: age } } console.log(sayHello.apply(obj,[24]));// {name: "jawil", age: 24}
这些都是小问题,想到了,就很好解决。咱们来看看此时的第三版apply
模拟方法。
//原生JavaScript封装apply方法,第三版 Function.prototype.applyThree = function(context) { var context = context || window var args = arguments[1] //获取传入的数组参数 context.fn = this //假想context对象预先不存在名为fn的属性 if (args == void 0) { //没有传入参数直接执行 return context.fn() } 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 returnValue = eval(fnStr) //仍是eval强大 delete context.fn //执行完毕以后删除这个属性 return returnValue }
好紧张,再来作个小测试,demo,应该不会出问题:
var obj = { name: 'jawil' } function sayHello(age) { return { name: this.name, age: age } } console.log(sayHello.applyThree(obj,[24]));// 完美输出{name: "jawil", age: 24}
完美?perfact?这就行了,不存在的,咱们来看看第四步的实现。
其实一开始就埋下了一个隐患,咱们看看这段代码:
Function.prototype.applyThree = function(context) { var context = context || window var args = arguments[1] //获取传入的数组参数 context.fn = this //假想context对象预先不存在名为fn的属性 ...... }
就是这句话, context.fn = this //假想context对象预先不存在名为fn的属性
,这就是一开始的隐患,咱们只是假设,可是并不能防止contenx
对象一开始就没有这个属性,要想作到完美,就要保证这个context.fn
中的fn
的惟一性。
因而我天然而然的想到了强大的ES6
,这玩意仍是好用啊,幸亏早就了解并一直在使用ES6
,尚未学习过ES6的童鞋赶忙学习一下,没有坏处的。
从新复习下新知识:
基本数据类型有6种:Undefined
、Null
、布尔值(Boolean)
、字符串(String)
、数值(Number)
、对象(Object)
。
ES5对象属性名都是字符串容易形成属性名的冲突。
举个栗子?:
var a = { name: 'jawil'}; a.name = 'lulin'; //这样就会重写属性
ES6
引入了一种新的原始数据类型Symbol
,表示独一无二的值。
注意,Symbol
函数前不能使用new
命令,不然会报错。这是由于生成的Symbol
是一个原始类型的值,不是对象
Symbol
函数能够接受一个字符串做为参数,表示对Symbol
实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。
// 没有参数的状况 var s1 = Symbol(); var s2 = Symbol(); s1 === s2 // false // 有参数的状况 var s1 = Symbol("foo"); var s2 = Symbol("foo"); s1 === s2 // false
注意:
Symbol
值不能与其余类型的值进行运算。
做为属性名的Symbol
var mySymbol = Symbol(); // 第一种写法 var a = {}; a[mySymbol] = 'Hello!'; // 第二种写法 var a = { [mySymbol]: 'Hello!' }; // 第三种写法 var a = {}; Object.defineProperty(a, mySymbol, { value: 'Hello!' }); // 以上写法都获得一样结果 a[mySymbol] // "Hello!"
注意,Symbol值做为对象属性名时,不能用点运算符。
看看下面这个栗子?:
var a = {}; var name = Symbol(); a.name = 'jawil'; a[name] = 'lulin'; console.log(a.name,a[name]); //jawil,lulin
Symbol
值做为属性名时,该属性仍是公开属性,不是私有属性。
这个有点相似于java
中的protected
属性(protected和private的区别:在类的外部都是不能够访问的,在类内的子类能够继承protected不能够继承private)
可是这里的Symbol在类外部也是能够访问的,只是不会出如今for...in
、for...of
循环中,也不会被Object.keys()
、Object.getOwnPropertyNames()
返回。但有一个Object.getOwnPropertySymbols
方法,能够获取指定对象的全部Symbol
属性名。
看看第四版的实现demo,想必你们了解上面知识已经猜获得怎么写了,很简单。
直接加个var fn = Symbol()
就好了,,,
//原生JavaScript封装apply方法,第四版 Function.prototype.applyFour = function(context) { var context = context || window var args = arguments[1] //获取传入的数组参数 var fn = Symbol() context[fn] = this //假想context对象预先不存在名为fn的属性 if (args == void 0) { //没有传入参数直接执行 return context[fn]() } 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 returnValue = eval(fnStr) //仍是eval强大 delete context[fn] //执行完毕以后删除这个属性 return returnValue }
呃呃呃额额,慢着,ES3
就出现的方法,你用ES6
来实现,你好意思么?你可能会说,无论黑猫白猫,只要能抓住老鼠的猫就是好猫,面试官直说不许用call
和apply
方法可是没说不许用ES6
语法啊。
反正公说公有理婆说婆有理,这里仍是不用Symbol
方法实现一下,咱们知道,ES6其实都是语法糖,ES6
能写的,咋们ES5
都能实现,这就致使了babel
这类把ES6
语法转化成ES5
的代码了。
至于babel
把Symbol
属性转换成啥代码了,我也没去看,有兴趣的能够看一下稍微研究一下,这里我说一下简单的模拟。
ES5
没有 Sybmol
,属性名称只多是一个字符串,若是咱们能作到这个字符串不可预料,那么就基本达到目标。要达到不可预期,一个随机数基本上就解决了。
//简单模拟Symbol属性 function jawilSymbol(obj) { var unique_proper = "00" + Math.random(); if (obj.hasOwnProperty(unique_proper)) { arguments.callee(obj)//若是obj已经有了这个属性,递归调用,直到没有这个属性 } else { return unique_proper; } } //原生JavaScript封装apply方法,第五版 Function.prototype.applyFive = function(context) { var context = context || window var args = arguments[1] //获取传入的数组参数 var fn = jawilSymbol(context); context[fn] = this //假想context对象预先不存在名为fn的属性 if (args == void 0) { //没有传入参数直接执行 return context[fn]() } 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 returnValue = eval(fnStr) //仍是eval强大 delete context[fn] //执行完毕以后删除这个属性 return returnValue }
好紧张,再来作个小测试,demo,应该不会出问题:
var obj = { name: 'jawil' } function sayHello(age) { return { name: this.name, age: age } } console.log(sayHello.applyFive(obj,[24]));// 完美输出{name: "jawil", age: 24}
到此,咱们完成了apply的模拟实现,给本身一个赞 b( ̄▽ ̄)d
这个不须要讲了吧,道理都同样,就是参数同样,这里我给出我实现的一种方式,看不懂,本身写一个去。
//原生JavaScript封装call方法 Function.prototype.callOne = function(context) { return this.applyFive(([].shift.applyFive(arguments), arguments) //巧妙地运用上面已经实现的applyFive函数 }
看不太明白也不能怪我咯,我就不细讲了,看个demo证实一下,这个写法没问题。
Function.prototype.applyFive = function(context) {//刚才写的一大串} Function.prototype.callOne = function(context) { return this.applyFive(([].shift.applyFive(arguments)), arguments) //巧妙地运用上面已经实现的applyFive函数 } //测试一下 var obj = { name: 'jawil' } function sayHello(age) { return { name: this.name, age: age } } console.log(sayHello.callOne(obj,24));// 完美输出{name: "jawil", age: 24}
养兵千日,用兵一时。
若是掌握了上面实现apply
的方法,我想理解起来模拟实现bind
方法也是垂手可得,原理都差很少,咱们仍是来看看bind
方法的定义。
咱们仍是简单的看下ECMAScript
规范对bind
方法的定义,暂时看不懂没关系,获取几个关键信息就行。
15.3.4.5 Function.prototype.bind (thisArg [, arg1 [, arg2, …]])
注意一点,ECMAScript规范提到: Function.prototype.bind 建立的函数对象不包含 prototype 属性或 [[Code]], [[FormalParameters]], [[Scope]] 内部属性。
bind() 方法会建立一个新函数,当这个新函数被调用时,它的
this
值是传递给bind()
的第一个参数, 它的参数是bind()
的其余参数和其本来的参数,bind返回的绑定函数也能使用new操做符建立对象:这种行为就像把原函数当成构造器。提供的this值被忽略,同时调用时的参数被提供给模拟函数。。
语法是这样样子的:fun.bind(thisArg[, arg1[, arg2[, ...]]])
呃呃呃,是否是似曾相识,这不是call方法的语法一个样子么,,,但它们是同样的吗?
bind方法传递给调用函数的参数能够逐个列出,也能够写在数组中。bind方法与call、apply最大的不一样就是前者返回一个绑定上下文的函数,然后二者是直接执行了函数。因为这个缘由,上面的代码也能够这样写:
jawil.sayHello.bind(lulin)(24); //hello, i am lulin 24 years old jawil.sayHello.bind(lulin)([24]); //hello, i am lulin 24 years old
bind方法还能够这样写 fn.bind(obj, arg1)(arg2)
.
用一句话总结bind的用法:该方法建立一个新函数,称为绑定函数,绑定函数会以建立它时传入bind方法的第一个参数做为this,传入bind方法的第二个以及之后的参数加上绑定函数运行时自己的参数按照顺序做为原函数的参数来调用原函数。
实际使用中咱们常常会碰到这样的问题:
function Person(name){ this.nickname = name; this.distractedGreeting = function() { setTimeout(function(){ console.log("Hello, my name is " + this.nickname); }, 500); } } var alice = new Person('jawil'); alice.distractedGreeting(); //Hello, my name is undefined
这个时候输出的this.nickname是undefined,缘由是this指向是在运行函数时肯定的,而不是定义函数时候肯定的,再由于setTimeout在全局环境下执行,因此this指向setTimeout的上下文:window。关于this指向问题,这里就不细扯
之前解决这个问题的办法一般是缓存this
,例如:
function Person(name){ this.nickname = name; this.distractedGreeting = function() { var self = this; // <-- 注意这一行! setTimeout(function(){ console.log("Hello, my name is " + self.nickname); // <-- 还有这一行! }, 500); } } var alice = new Person('jawil'); alice.distractedGreeting(); // after 500ms logs "Hello, my name is jawil"
这样就解决了这个问题,很是方便,由于它使得setTimeout函数中能够访问Person的上下文。可是看起来稍微一种蛋蛋的忧伤。
可是如今有一个更好的办法!您可使用bind
。上面的例子中被更新为:
function Person(name){ this.nickname = name; this.distractedGreeting = function() { setTimeout(function(){ console.log("Hello, my name is " + this.nickname); }.bind(this), 500); // <-- this line! } } var alice = new Person('jawil'); alice.distractedGreeting(); // after 500ms logs "Hello, my name is jawil"
bind() 最简单的用法是建立一个函数,使这个函数不论怎么调用都有一样的 this 值。JavaScript新手常常犯的一个错误是将一个方法从对象中拿出来,而后再调用,但愿方法中的 this 是原来的对象。(好比在回调中传入这个方法。)若是不作特殊处理的话,通常会丢失原来的对象。从原来的函数和原来的对象建立一个绑定函数,则能很漂亮地解决这个问题:
this.x = 9; var module = { x: 81, getX: function() { return this.x; } }; module.getX(); // 81 var getX = module.getX; getX(); // 9, 由于在这个例子中,"this"指向全局对象 // 建立一个'this'绑定到module的函数 var boundGetX = getX.bind(module); boundGetX(); // 81
很不幸,Function.prototype.bind 在IE8及如下的版本中不被支持,因此若是你没有一个备用方案的话,可能在运行时会出现问题。bind 函数在 ECMA-262 第五版才被加入;它可能没法在全部浏览器上运行。你能够部份地在脚本开头加入如下代码,就能使它运做,让不支持的浏览器也能使用 bind() 功能。
幸运的是,咱们能够本身来模拟bind
功能:
了解了以上内容,咱们来实现一个初级的bind
函数Polyfill
:
Function.prototype.bind = function (context) { var me = this; var argsArray = Array.prototype.slice.callOne(arguments); return function () { return me.applyFive(context, argsArray.slice(1)) } }
咱们先简要解读一下:
基本原理是使用apply
进行模拟。函数体内的this
,就是须要绑定this
的实例函数,或者说是原函数。最后咱们使用apply
来进行参数(context)绑定,并返回。
同时,将第一个参数(context)之外的其余参数,做为提供给原函数的预设参数,这也是基本的“颗粒化(curring)”基础。
上面的实现(包括后面的实现),实际上是一个典型的“Monkey patching(猴子补丁)”,即“给内置对象扩展方法”。因此,若是面试者能进行一下“嗅探”,进行兼容处理,就是锦上添花了。
Function.prototype.bind = Function.prototype.bind || function (context) { ... }
对于函数的柯里化不太了解的童鞋,能够先尝试读读这篇文章:前端基础进阶(八):深刻详解函数的柯里化。
上述的实现方式中,咱们返回的参数列表里包含:atgsArray.slice(1)
,他的问题在于存在预置参数功能丢失的现象。
想象咱们返回的绑定函数中,若是想实现预设传参(就像bind
所实现的那样),就面临尴尬的局面。真正实现颗粒化的“完美方式”是:
Function.prototype.bind = Function.prototype.bind || function (context) { var me = this; var args = Array.prototype.slice.callOne(arguments, 1); return function () { var innerArgs = Array.prototype.slice.callOne(arguments); var finalArgs = args.concat(innerArgs); return me.applyFive(context, finalArgs); } }
上面什么是bind函数还介绍到:bind返回的函数若是做为构造函数,搭配new关键字出现的话,咱们的绑定this就须要“被忽略”。
有了上边的讲解,不难理解须要兼容构造函数场景的实现:
Function.prototype.bind = Function.prototype.bind || function (context) { var me = this; var args = Array.prototype.slice.callOne(arguments, 1); var F = function () {}; F.prototype = this.prototype; var bound = function () { var innerArgs = Array.prototype.slice.callOne(arguments); var finalArgs = args.concat(innerArgs); return me.apply(this instanceof F ? this : context || this, finalArgs); } bound.prototype = new F(); return bound; }
咱们须要调用bind
方法的必定要是一个函数,因此能够在函数体内作一个判断:
if (typeof this !== "function") { throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); }
作到全部这一切,基本算是完成了。其实MDN上有个本身实现的polyfill,就是如此实现的。
另外,《JavaScript Web Application》一书中对bind()的实现,也是如此。
//简单模拟Symbol属性 function jawilSymbol(obj) { var unique_proper = "00" + Math.random(); if (obj.hasOwnProperty(unique_proper)) { arguments.callee(obj)//若是obj已经有了这个属性,递归调用,直到没有这个属性 } else { return unique_proper; } } //原生JavaScript封装apply方法,第五版 Function.prototype.applyFive = function(context) { var context = context || window var args = arguments[1] //获取传入的数组参数 var fn = jawilSymbol(context); context[fn] = this //假想context对象预先不存在名为fn的属性 if (args == void 0) { //没有传入参数直接执行 return context[fn]() } 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 returnValue = eval(fnStr) //仍是eval强大 delete context[fn] //执行完毕以后删除这个属性 return returnValue } //简单模拟call函数 Function.prototype.callOne = function(context) { return this.applyFive(([].shift.applyFive(arguments)), arguments) //巧妙地运用上面已经实现的applyFive函数 } //简单模拟bind函数 Function.prototype.bind = Function.prototype.bind || function (context) { var me = this; var args = Array.prototype.slice.callOne(arguments, 1); var F = function () {}; F.prototype = this.prototype; var bound = function () { var innerArgs = Array.prototype.slice.callOne(arguments); var finalArgs = args.concat(innerArgs); return me.applyFive(this instanceof F ? this : context || this, finalArgs); } bound.prototype = new F(); return bound; }
好紧张,最后来作个小测试,demo,应该不会出问题:
var obj = { name: 'jawil' } function sayHello(age) { return { name: this.name, age: age } } console.log(sayHello.bind(obj,24)());// 完美输出{name: "jawil", age: 24}
看了这篇文章,之后再遇到相似的问题,应该可以顺利经过吧~