Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

不用call和apply方法模拟实现ES5的bind方法 #16

Open
jawil opened this issue May 2, 2017 · 32 comments
Open

不用call和apply方法模拟实现ES5的bind方法 #16

jawil opened this issue May 2, 2017 · 32 comments

Comments

@jawil
Copy link
Owner

jawil commented May 2, 2017

本文首发我的个人博客:前端小密圈,评论交流送1024邀请码,嘿嘿嘿😄。

来自朋友去某信用卡管家的做的一道面试题,用原生JavaScript模拟ES5bind方法,不准用callbind方法。

至于结果嘛。。。那个人当然是没写出来,我就自己尝试研究了一番,其实早就写了,一直没有组织好语言发出来。

额。。。这个题有点刁钻,这是对JavaScript基本功很好的一个检测,看你JavaScript掌握的怎么样以及平时有没有去深入研究一些方法的实现,简而言之,就是有没有折腾精神。

不准用不用callapply方法,这个没啥好说的,不准用我们就用原生JavaScript先来模拟一个apply方法,感兴趣的童鞋也可以看看chromev8怎么实现这个方法的,这里我只按照自己的思维实现,在模拟之前我们先要明白和了解原生callapply方法是什么。

简单粗暴地来说,callapplybind是用于绑定this指向的。(如果你还不了解JS中this的指向问题,以及执行环境上下文的奥秘,这篇文章暂时就不太适合阅读)。

什么是call和apply方法

我们单独看看ECMAScript规范对apply的定义,看个大概就行:

15.3.4.3 Function.prototype.apply (thisArg, argArray)

顺便贴一贴中文版,免得翻译一下,中文版地址

通过定义简单说一下call和apply方法,他们就是参数不同,作用基本相同。

1、每个函数都包含两个非继承而来的方法:apply()和call()。
2、他们的用途相同,都是在特定的作用域中调用函数。
3、接收参数方面不同,apply()接收两个参数,一个是函数运行的作用域(this),另一个是参数数组。
4、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

然后看看使用applycall之后的输出:

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则是要写在数组中。

总结一句话介绍callapply

call()方法在使用一个指定的this值和若干个指定的参数值的前提下调用某个函数或方法。
apply()方法在使用一个指定的this值和参数值必须是数组类型的前提下调用某个函数或方法。

分析call和apply的原理

上面代码,我们注意到了两点:

  1. callapply改变了this的指向,指向到lulin
  2. sayHello函数执行了

这里默认大家都对this有一个基本的了解,知道什么时候this该指向谁,我们结合这两句话来分析这个通用函数:f.apply(o),我们直接看一本书对其中原理的解读,具体什么书,我也不知道,参数我们先不管,先了解其中的大致原理。

注意红色框中的部分,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一下。

实现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的一些奇淫方法,不过applyES3的方法,我们为了模拟实现一个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种:UndefinedNull布尔值(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...infor...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来实现,你好意思么?你可能会说,不管黑猫白猫,只要能抓住老鼠的猫就是好猫,面试官直说不准用callapply方法但是没说不准用ES6语法啊。

反正公说公有理婆说婆有理,这里还是不用Symbol方法实现一下,我们知道,ES6其实都是语法糖,ES6能写的,咋们ES5都能实现,这就导致了babel这类把ES6语法转化成ES5的代码了。

至于babelSymbol属性转换成啥代码了,我也没去看,有兴趣的可以看一下稍微研究一下,这里我说一下简单的模拟。

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

实现Call方法

这个不需要讲了吧,道理都一样,就是参数一样,这里我给出我实现的一种方式,看不懂,自己写一个去。

//原生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}

实现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]] 内部属性。

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方法的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数。

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) {
    ...
}

颗粒化(curring)实现

对于函数的柯里化不太了解的童鞋,可以先尝试读读这篇文章:前端基础进阶(八):深入详解函数的柯里化
上述的实现方式中,我们返回的参数列表里包含: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}

看了这篇文章,以后再遇到类似的问题,应该能够顺利通过吧~

参考文章

ES6入门之Symbol
ECMAScript 5.1(英文版)
从一道面试题,到“我可能看了假源码”

@mqyqingfeng
Copy link

好巧,我们今天的文章同时讲到了call和apply的模拟实现

@mqyqingfeng
Copy link

不过现在的bind的模拟实现其实是有个小问题的,我专门私信了颜海镜求证了这个问题。

@jawil
Copy link
Owner Author

jawil commented May 2, 2017

length问题?@mqyqingfeng 同事朋友去51信用卡遇到这个面试题,我上周写了一半,今天看见你发了,我也赶紧写完,发现思路都差不多,我主要是先实现apply引出的。。。😄

@mqyqingfeng
Copy link

并不是哦,如果你比较MDN英文版和中文版的实现,就会发现这个问题。先卖个关子,明天发布的文章讲解bind的模拟实现就会讲到这个问题。(๑•̀ㅂ•́)و✧

@jawil
Copy link
Owner Author

jawil commented May 2, 2017

恩恩,我会关注呢,反正你一发就会有邮件推送😄

@jawil jawil changed the title 可能遇到假的面试题:不用call和apply方法模拟实现ES5的bind方法 不用call和apply方法模拟实现ES5的bind方法 May 2, 2017
@lzlu
Copy link

lzlu commented May 2, 2017

个人优化后的版本

Function.prototype.applyOne = function(context){
    context.fn = this;
    eval('context.fn('+(arguments[1]||[]).toString()+')');
    delete context.fn;
}

(我也算参与过1.2k的大项目了么

@mqyqingfeng
Copy link

仔细看了你的这篇文章,嗯,觉得……哈哈,建议楼主在整理资料的过程中慢慢找到自己的思路去表达,否则按照参考文章的思路去写,容易显得似曾相识。我自己也有这样的问题,与楼主共勉。

@xumeiyan
Copy link

xumeiyan commented May 3, 2017

看了有些文章,最后那个三元判断是这种的
this instanceof F ? this : context || window
有点迷糊

@jawil
Copy link
Owner Author

jawil commented May 3, 2017

在非严格模式下,走后面逻辑的话此时的this就是指向window,其实是一个东西。。。@xumeiyan

@Mryszhang
Copy link

sayHello.applyFive(obj,['hello']是有问题的,eval解析时hello未定义,用toString也是不行的

@jawil
Copy link
Owner Author

jawil commented Jun 7, 2017

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

@Thinking80s
Copy link

都是基础啊,理解原理了自然就可以写出来了

@leat14536
Copy link

大多数(可能是所有)原生对象上的方法的prototypeundefined, 比如 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);

@jawil
Copy link
Owner Author

jawil commented Sep 4, 2017

感谢补充优化,不过Object.create(null)好像是ES5的方法,用在这里hack好像不太合适,不过也没事,其实Object.create的也很好模拟实现 @leat14536

@axuebin
Copy link

axuebin commented Sep 19, 2017

请教一个问题,您实现apply的第三步中通过以下方式来实现当this为空时指向window

var context = context || window

可是,当this为空时,apply应该是不能传递参数的吧?

applyFive([1,2])这样是不行的

这块我有点不明白,请您指教一下。。

@mqyqingfeng
Copy link

@axuebin 处理的是这种情况 fn.apply(null, [1, 2])

@jawil
Copy link
Owner Author

jawil commented Sep 19, 2017

感谢@mqyqingfeng 百忙之中作出回答😂

@axuebin
Copy link

axuebin commented Sep 19, 2017

明白了。。我之前有一个地方写错了,导致apply()即没this又没参数时出错了。。谢谢~

@lkangd
Copy link

lkangd commented Oct 26, 2017

不错不错,厉害,不过applyFive没传参的直接执行的时候没有执行delete context[fn]。

    if (args == void 0) { //没有传入参数直接执行
        var returnValue = context[fn]();
        delete context[fn];
        return returnValue;
    }

@bravepg
Copy link

bravepg commented Nov 29, 2017

image

博主真厉害,按照您的步骤进行到如图标红的一步,发现args
image
是上图中的第一行代码(是个对象),但是却有个length属性,能不能解释一下为什么啊?

@mqyqingfeng
Copy link

@gaopeng0108 arguments 对象是一个类数组对象,就是会有一个 length 属性,你在浏览器中可以看到这个属性,可以参考这篇文章 mqyqingfeng/Blog#14

@zyg-github
Copy link

zyg-github commented Apr 15, 2018

原生的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 += ')'

@jawil
Copy link
Owner Author

jawil commented Apr 15, 2018

当初考虑的情况确实只是最简单的字符串,因为只是提供一种实现的思维,感谢指正 @zyg-github

@hsroad
Copy link

hsroad commented Jul 19, 2018

飚一句zanghua,前端感觉真TM任重而道远

@BlaStone
Copy link

apply在没有传参的时候直接返回了contextfn,但是好像没有把context的fn删除呢

@lesini
Copy link

lesini commented Oct 31, 2018

厉害!

@jiamianmao
Copy link

莫非你就是1024康先生,麻烦送一码。1024。

@lizhongzhen11
Copy link

哈哈哈哈哈,我前几天在某个群里也看到这条题目,我今天想了半天,偶然意识到可以这样:

// ...省略
obj.fn = fn
let result = obj.fn(...arr)
delete obj.fn
return result

我写完觉得这样是不是不好,然后我就网上搜,发现你大致也是这样做的,开心~

@tgzzl
Copy link

tgzzl commented Jan 20, 2022 via email

@chuanHH
Copy link

chuanHH commented Jan 20, 2022 via email

@LiXinLeLe
Copy link

bind方法传递给调用函数的参数可以逐个列出,也可以写在数组中。

这句感觉不太对吧,需要逐个列出,不可以放在数组中

@chuanHH
Copy link

chuanHH commented Jun 28, 2022 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests