JavaScript 深入之bind的模拟实现

2017/05/26 · JavaScript
· bind

原文出处: 冴羽   

已离开简书,原因参见
http://www.jianshu.com/p/0f12350a6b66。

原文出处

JavaScript深入之bind的模拟实现

JavaScript 深入之call和apply的模拟实现

2017/05/25 · JavaScript
· apply,
call

原文出处: 冴羽   

JavaScript 深入之new的模拟实现

2017/05/26 · JavaScript
· new

原文出处: 冴羽   

bind

一句话介绍 bind:

bind() 方法会创建一个新函数。当这个新函数被调用时,bind()
的第一个参数将作为它运行时的
this,之后的一序列参数将会在传递的实参前传入作为它的参数。(来自于 MDN
)

由此我们可以首先得出 bind 函数的两个特点:

  1. 返回一个函数
  2. 可以传入参数

虽人微言轻,但也要有自己的态度。

bind

我们在模拟 bind之前,先看看 bind实现了哪些功能。

        var foo = {
            value: 1
        };

        function bar() {
            console.log(this.value);
        }

        // 返回了一个函数
        var bindFoo = bar.bind(foo);

        bindFoo(); // 1

由此我们可以首先得出 bind 函数的两个特点:

  1. 返回一个函数
  2. 改变了this的指向

call

一句话介绍 call:

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

举个例子:

var foo = { value: 1 }; function bar() { console.log(this.value); }
bar.call(foo); // 1

1
2
3
4
5
6
7
8
9
var foo = {
    value: 1
};
 
function bar() {
    console.log(this.value);
}
 
bar.call(foo); // 1

注意两点:

  1. call 改变了 this 的指向,指向到 foo
  2. bar 函数执行了

new

一句话介绍 new:

new
运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象类型之一

也许有点难懂,我们在模拟 new 之前,先看看 new 实现了哪些功能。

举个例子:

// Otaku 御宅族,简称宅 function Otaku (name, age) { this.name = name;
this.age = age; this.habit = ‘Games’; } //
因为缺乏锻炼的缘故,身体强度让人担忧 Otaku.prototype.strength = 60;
Otaku.prototype.sayYourName = function () { console.log(‘I am ‘ +
this.name); } var person = new Otaku(‘Kevin’, ’18’);
console.log(person.name) // Kevin console.log(person.habit) // Games
console.log(person.strength) // 60 person.sayYourName(); // I am Kevin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Otaku 御宅族,简称宅
function Otaku (name, age) {
    this.name = name;
    this.age = age;
 
    this.habit = ‘Games’;
}
 
// 因为缺乏锻炼的缘故,身体强度让人担忧
Otaku.prototype.strength = 60;
 
Otaku.prototype.sayYourName = function () {
    console.log(‘I am ‘ + this.name);
}
 
var person = new Otaku(‘Kevin’, ’18’);
 
console.log(person.name) // Kevin
console.log(person.habit) // Games
console.log(person.strength) // 60
 
person.sayYourName(); // I am Kevin

从这个例子中,我们可以看到,实例 person 可以:

  1. 访问到 Otaku 构造函数里的属性
  2. 访问到 Otaku.prototype 中的属性

接下来,我们可以尝试着模拟一下了。

因为 new 是关键字,所以无法像 bind
函数一样直接覆盖,所以我们写一个函数,命名为 objectFactory,来模拟 new
的效果。用的时候是这样的:

function Otaku () { …… } // 使用 new var person = new Otaku(……); // 使用
objectFactory var person = objectFactory(Otaku, ……)

1
2
3
4
5
6
7
8
function Otaku () {
    ……
}
 
// 使用 new
var person = new Otaku(……);
// 使用 objectFactory
var person = objectFactory(Otaku, ……)

返回函数的模拟实现

从第一个特点开始,我们举个例子:

var foo = { value: 1 }; function bar() { console.log(this.value); } //
返回了一个函数 var bindFoo = bar.bind(foo); bindFoo(); // 1

1
2
3
4
5
6
7
8
9
10
11
12
var foo = {
    value: 1
};
 
function bar() {
    console.log(this.value);
}
 
// 返回了一个函数
var bindFoo = bar.bind(foo);
 
bindFoo(); // 1

关于指定 this 的指向,我们可以使用 call 或者 apply 实现,关于 call 和
apply
的模拟实现,可以查看《JavaScript深入之call和apply的模拟实现》。我们来写第一版的代码:

// 第一版 Function.prototype.bind2 = function (context) { var self =
this; return function () { self.apply(context); } }

1
2
3
4
5
6
7
8
// 第一版
Function.prototype.bind2 = function (context) {
    var self = this;
    return function () {
        self.apply(context);
    }
 
}

文章可以在我的 Github
https://github.com/mqyqingfeng/Blog
查看

模拟实现第一步


          var foo = {
            value: 1
        };

        function bar() {
            console.log(this.value);
        }
        // 第一版
        Function.prototype.bind2 = function (context) {
            var self = this;
            return function () {  // 返回一个函数
            return self.apply(context);  // 改变绑定函数的this指向
            }

        }

        // 返回了一个函数
        var bindFoo = bar.bind2(foo);
        bindFoo();  // 1

模拟实现第一步

那么我们该怎么模拟实现这两个效果呢?

试想当调用 call 的时候,把 foo 对象改造成如下:

var foo = { value: 1, bar: function() { console.log(this.value) } };
foo.bar(); // 1

1
2
3
4
5
6
7
8
var foo = {
    value: 1,
    bar: function() {
        console.log(this.value)
    }
};
 
foo.bar(); // 1

这个时候 this 就指向了 foo,是不是很简单呢?

但是这样却给 foo 对象本身添加了一个属性,这可不行呐!

不过也不用担心,我们用 delete 再删除它不就好了~

所以我们模拟的步骤可以分为:

  1. 将函数设为对象的属性
  2. 执行该函数
  3. 删除该函数

以上个例子为例,就是:

// 第一步 foo.fn = bar // 第二步 foo.fn() // 第三步 delete foo.fn

1
2
3
4
5
6
// 第一步
foo.fn = bar
// 第二步
foo.fn()
// 第三步
delete foo.fn

fn 是对象的属性名,反正最后也要删除它,所以起成什么都无所谓。

根据这个思路,我们可以尝试着去写第一版的 call2 函数:

// 第一版 Function.prototype.call2 = function(context) { //
首先要获取调用call的函数,用this可以获取 context.fn = this;
context.fn(); delete context.fn; } // 测试一下 var foo = { value: 1 };
function bar() { console.log(this.value); } bar.call2(foo); // 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 第一版
Function.prototype.call2 = function(context) {
    // 首先要获取调用call的函数,用this可以获取
    context.fn = this;
    context.fn();
    delete context.fn;
}
 
// 测试一下
var foo = {
    value: 1
};
 
function bar() {
    console.log(this.value);
}
 
bar.call2(foo); // 1

正好可以打印 1 哎!是不是很开心!(~ ̄▽ ̄)~

初步实现

分析:

因为 new
的结果是一个新对象,所以在模拟实现的时候,我们也要建立一个新对象,假设这个对象叫
obj,因为 obj 会具有 Otaku
构造函数里的属性,想想经典继承的例子,我们可以使用 Otaku.apply(obj,
arguments)来给 obj 添加新的属性。

在 JavaScript 深入系列第一篇中,我们便讲了原型与原型链,我们知道实例的
__proto__ 属性会指向构造函数的
prototype,也正是因为建立起这样的关系,实例可以访问原型上的属性。

现在,我们可以尝试着写第一版了:

// 第一版代码 function objectFactory() { var obj = new Object(),
Constructor = [].shift.call(arguments); obj.__proto__ =
Constructor.prototype; Constructor.apply(obj, arguments); return obj; };

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 第一版代码
function objectFactory() {
 
    var obj = new Object(),
 
    Constructor = [].shift.call(arguments);
 
    obj.__proto__ = Constructor.prototype;
 
    Constructor.apply(obj, arguments);
 
    return obj;
 
};

在这一版中,我们:

  1. 用new Object() 的方式新建了一个对象 obj
  2. 取出第一个参数,就是我们要传入的构造函数。此外因为 shift
    会修改原数组,所以 arguments 会被去除第一个参数
  3. 将 obj 的原型指向构造函数,这样 obj 就可以访问到构造函数原型中的属性
  4. 使用 apply,改变构造函数 this 的指向到新建的对象,这样 obj
    就可以访问到构造函数中的属性
  5. 返回 obj

更多关于:

原型与原型链,可以看《JavaScript深入之从原型到原型链》

apply,可以看《JavaScript深入之call和apply的模拟实现》

经典继承,可以看《JavaScript深入之继承》

复制以下的代码,到浏览器中,我们可以做一下测试:

function Otaku (name, age) { this.name = name; this.age = age;
this.habit = ‘Games’; } Otaku.prototype.strength = 60;
Otaku.prototype.sayYourName = function () { console.log(‘I am ‘ +
this.name); } function objectFactory() { var obj = new Object(),
Constructor = [].shift.call(arguments); obj.__proto__ =
Constructor.prototype; Constructor.apply(obj, arguments); return obj; };
var person = objectFactory(Otaku, ‘Kevin’, ’18’)
console.log(person.name) // Kevin console.log(person.habit) // Games
console.log(person.strength) // 60 person.sayYourName(); // I am Kevin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function Otaku (name, age) {
    this.name = name;
    this.age = age;
 
    this.habit = ‘Games’;
}
 
Otaku.prototype.strength = 60;
 
Otaku.prototype.sayYourName = function () {
    console.log(‘I am ‘ + this.name);
}
 
function objectFactory() {
    var obj = new Object(),
    Constructor = [].shift.call(arguments);
    obj.__proto__ = Constructor.prototype;
    Constructor.apply(obj, arguments);
    return obj;
};
 
var person = objectFactory(Otaku, ‘Kevin’, ’18’)
 
console.log(person.name) // Kevin
console.log(person.habit) // Games
console.log(person.strength) // 60
 
person.sayYourName(); // I am Kevin

[]~( ̄▽ ̄)~**

传参的模拟实现

接下来看第二点,可以传入参数。这个就有点让人费解了,我在 bind
的时候,是否可以传参呢?我在执行 bind
返回的函数的时候,可不可以传参呢?让我们看个例子:

var foo = { value: 1 }; function bar(name, age) {
console.log(this.value); console.log(name); console.log(age); } var
bindFoo = bar.bind(foo, ‘daisy’); bindFoo(’18’); // 1 // daisy // 18

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var foo = {
    value: 1
};
 
function bar(name, age) {
    console.log(this.value);
    console.log(name);
    console.log(age);
 
}
 
var bindFoo = bar.bind(foo, ‘daisy’);
bindFoo(’18’);
// 1
// daisy
// 18

函数需要传 name 和 age 两个参数,竟然还可以在 bind 的时候,只传一个
name,在执行返回的函数的时候,再传另一个参数 age!

这可咋办?不急,我们用 arguments 进行处理:

// 第二版 Function.prototype.bind2 = function (context) { var self =
this; // 获取bind2函数从第二个参数到最后一个参数 var args =
Array.prototype.slice.call(arguments, 1); return function () { //
这个时候的arguments是指bind返回的函数传入的参数 var bindArgs =
Array.prototype.slice.call(arguments); self.apply(context,
args.concat(bindArgs)); } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 第二版
Function.prototype.bind2 = function (context) {
 
    var self = this;
    // 获取bind2函数从第二个参数到最后一个参数
    var args = Array.prototype.slice.call(arguments, 1);
 
    return function () {
        // 这个时候的arguments是指bind返回的函数传入的参数
        var bindArgs = Array.prototype.slice.call(arguments);
        self.apply(context, args.concat(bindArgs));
    }
 
}

模拟实现第二步


举个例子:

        var foo = {
            value: 1
        };

        function bar(name, age) {
            console.log(this.value);
            console.log(name);
            console.log(age);

        }

        var bindFoo = bar.bind(foo, 'daisy');
        bindFoo('18');
        // 1
        // daisy
        // 18

由此我们可以看出:

  1. bind 绑定的时候可以传参数
  2. 在执行返回函数的时候也可以传参数

我们可以用 arguments 模拟,如下:

        var foo = {
            value: 1
        };

        function bar(name, age) {
            console.log(this.value);
            console.log(name);
            console.log(age);

        }
        // 第二版
        Function.prototype.bind2 = function (context) {

            var self = this;
            // 获取bind2函数从第二个参数到最后一个参数
            var args = Array.prototype.slice.call(arguments, 1);

            return function () {
                // 这个时候的arguments是指bind返回的函数传入的参数
                var bindArgs = Array.prototype.slice.call(arguments);
                return self.apply(context, args.concat(bindArgs));
            }

        }

        // 返回了一个函数
        var bindFoo = bar.bind2(foo, 'daisy');
        bindFoo('18');
        // 1
        // daisy
        // 18

模拟实现第二步

最一开始也讲了,call 函数还能给定参数执行函数。举个例子:

var foo = { value: 1 }; function bar(name, age) { console.log(name)
console.log(age) console.log(this.value); } bar.call(foo, ‘kevin’, 18);
// kevin // 18 // 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var foo = {
    value: 1
};
 
function bar(name, age) {
    console.log(name)
    console.log(age)
    console.log(this.value);
}
 
bar.call(foo, ‘kevin’, 18);
// kevin
// 18
// 1

注意:传入的参数并不确定,这可咋办?

不急,我们可以从 Arguments
对象中取值,取出第二个到最后一个参数,然后放到一个数组里。

比如这样:

// 以上个例子为例,此时的arguments为: // arguments = { // 0: foo, // 1:
‘kevin’, // 2: 18, // length: 3 // } //
因为arguments是类数组对象,所以可以用for循环 var args = []; for(var i
= 1, len = arguments.length; i len; i++) { args.push(‘arguments[‘ + i +
‘]’); } // 执行后 args为 [foo, ‘kevin’, 18]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 以上个例子为例,此时的arguments为:
// arguments = {
//      0: foo,
//      1: ‘kevin’,
//      2: 18,
//      length: 3
// }
// 因为arguments是类数组对象,所以可以用for循环
var args = [];
for(var i = 1, len = arguments.length; i  len; i++) {
    args.push(‘arguments[‘ + i + ‘]’);
}
 
// 执行后 args为 [foo, ‘kevin’, 18]

不定长的参数问题解决了,我们接着要把这个参数数组放到要执行的函数的参数里面去。

// 将数组里的元素作为多个参数放进函数的形参里 context.fn(args.join(‘,’))
// (O_o)?? // 这个方法肯定是不行的啦!!!

1
2
3
4
// 将数组里的元素作为多个参数放进函数的形参里
context.fn(args.join(‘,’))
// (O_o)??
// 这个方法肯定是不行的啦!!!

也许有人想到用 ES6 的方法,不过 call 是 ES3 的方法,我们为了模拟实现一个
ES3 的方法,要用到ES6的方法,好像……,嗯,也可以啦。但是我们这次用 eval
方法拼成一个函数,类似于这样:

eval(‘context.fn(‘ + args +’)’)

1
eval(‘context.fn(‘ + args +’)’)

这里 args 会自动调用 Array.toString() 这个方法。

所以我们的第二版克服了两个大问题,代码如下:

// 第二版 Function.prototype.call2 = function(context) { context.fn =
this; var args = []; for(var i = 1, len = arguments.length; i len;
i++) { args.push(‘arguments[‘ + i + ‘]’); } eval(‘context.fn(‘ + args
+’)’); delete context.fn; } // 测试一下 var foo = { value: 1 }; function
bar(name, age) { console.log(name) console.log(age)
console.log(this.value); } bar.call2(foo, ‘kevin’, 18); // kevin // 18
// 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 第二版
Function.prototype.call2 = function(context) {
    context.fn = this;
    var args = [];
    for(var i = 1, len = arguments.length; i  len; i++) {
        args.push(‘arguments[‘ + i + ‘]’);
    }
    eval(‘context.fn(‘ + args +’)’);
    delete context.fn;
}
 
// 测试一下
var foo = {
    value: 1
};
 
function bar(name, age) {
    console.log(name)
    console.log(age)
    console.log(this.value);
}
 
bar.call2(foo, ‘kevin’, 18);
// kevin
// 18
// 1

(๑•̀ㅂ•́)و✧

返回值效果实现

接下来我们再来看一种情况,假如构造函数有返回值,举个例子:

function Otaku (name, age) { this.strength = 60; this.age = age; return
{ name: name, habit: ‘Games’ } } var person = new Otaku(‘Kevin’, ’18’);
console.log(person.name) // Kevin console.log(person.habit) // Games
console.log(person.strength) // undefined console.log(person.age) //
undefined

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Otaku (name, age) {
    this.strength = 60;
    this.age = age;
 
    return {
        name: name,
        habit: ‘Games’
    }
}
 
var person = new Otaku(‘Kevin’, ’18’);
 
console.log(person.name) // Kevin
console.log(person.habit) // Games
console.log(person.strength) // undefined
console.log(person.age) // undefined

在这个例子中,构造函数返回了一个对象,在实例 person
中只能访问返回的对象中的属性。

而且还要注意一点,在这里我们是返回了一个对象,假如我们只是返回一个基本类型的值呢?

再举个例子:

function Otaku (name, age) { this.strength = 60; this.age = age; return
‘handsome boy’; } var person = new Otaku(‘Kevin’, ’18’);
console.log(person.name) // undefined console.log(person.habit) //
undefined console.log(person.strength) // 60 console.log(person.age) //
18

1
2
3
4
5
6
7
8
9
10
11
12
13
function Otaku (name, age) {
    this.strength = 60;
    this.age = age;
 
    return ‘handsome boy’;
}
 
var person = new Otaku(‘Kevin’, ’18’);
 
console.log(person.name) // undefined
console.log(person.habit) // undefined
console.log(person.strength) // 60
console.log(person.age) // 18

结果完全颠倒过来,这次尽管有返回值,但是相当于没有返回值进行处理。

所以我们还需要判断返回的值是不是一个对象,如果是一个对象,我们就返回这个对象,如果没有,我们该返回什么就返回什么。

再来看第二版的代码,也是最后一版的代码:

// 第二版的代码 function objectFactory() { var obj = new Object(),
Constructor = [].shift.call(arguments); obj.__proto__ =
Constructor.prototype; var ret = Constructor.apply(obj, arguments);
return typeof ret === ‘object’ ? ret : obj; };

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 第二版的代码
function objectFactory() {
 
    var obj = new Object(),
 
    Constructor = [].shift.call(arguments);
 
    obj.__proto__ = Constructor.prototype;
 
    var ret = Constructor.apply(obj, arguments);
 
    return typeof ret === ‘object’ ? ret : obj;
 
};

构造函数效果的模拟实现

完成了这两点,最难的部分到啦!因为 bind 还有一个特点,就是

一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器。提供的
this 值被忽略,同时调用时的参数被提供给模拟函数。

也就是说当 bind 返回的函数作为构造函数的时候,bind 时指定的 this
值会失效,但传入的参数依然生效。举个例子:

var value = 2; var foo = { value: 1 }; function bar(name, age) {
this.habit = ‘shopping’; console.log(this.value); console.log(name);
console.log(age); } bar.prototype.friend = ‘kevin’; var bindFoo =
bar.bind(foo, ‘daisy’); var obj = new bindFoo(’18’); // undefined //
daisy // 18 console.log(obj.habit); console.log(obj.friend); // shopping
// kevin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var value = 2;
 
var foo = {
    value: 1
};
 
function bar(name, age) {
    this.habit = ‘shopping’;
    console.log(this.value);
    console.log(name);
    console.log(age);
}
 
bar.prototype.friend = ‘kevin’;
 
var bindFoo = bar.bind(foo, ‘daisy’);
 
var obj = new bindFoo(’18’);
// undefined
// daisy
// 18
console.log(obj.habit);
console.log(obj.friend);
// shopping
// kevin

注意:尽管在全局和 foo 中都声明了 value 值,最后依然返回了
undefind,说明绑定的 this 失效了,如果大家了解 new
的模拟实现,就会知道这个时候的 this 已经指向了 obj。

(哈哈,我这是为我的下一篇文章《JavaScript深入系列之new的模拟实现》打广告)。

所以我们可以通过修改返回的函数的原型来实现,让我们写一下:

// 第三版 Function.prototype.bind2 = function (context) { var self =
this; var args = Array.prototype.slice.call(arguments, 1); var fbound =
function () { var bindArgs = Array.prototype.slice.call(arguments); //
当作为构造函数时,this 指向实例,self 指向绑定函数,因为下面一句
`fbound.prototype = this.prototype;`,已经修改了 fbound.prototype 为
绑定函数的 prototype,此时结果为 true,当结果为 true 的时候,this
指向实例。 // 当作为普通函数时,this 指向 window,self
指向绑定函数,此时结果为 false,当结果为 false 的时候,this 指向绑定的
context。 self.apply(this instanceof self ? this : context,
args.concat(bindArgs)); } // 修改返回函数的 prototype 为绑定函数的
prototype,实例就可以继承函数的原型中的值 fbound.prototype =
this.prototype; return fbound; }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 第三版
Function.prototype.bind2 = function (context) {
    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);
 
    var fbound = function () {
 
        var bindArgs = Array.prototype.slice.call(arguments);
        // 当作为构造函数时,this 指向实例,self 指向绑定函数,因为下面一句 `fbound.prototype = this.prototype;`,已经修改了 fbound.prototype 为 绑定函数的 prototype,此时结果为 true,当结果为 true 的时候,this 指向实例。
        // 当作为普通函数时,this 指向 window,self 指向绑定函数,此时结果为 false,当结果为 false 的时候,this 指向绑定的 context。
        self.apply(this instanceof self ? this : context, args.concat(bindArgs));
    }
    // 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承函数的原型中的值
    fbound.prototype = this.prototype;
    return fbound;
}

如果对原型链稍有困惑,可以查看《JavaScript深入之从原型到原型链》。

模拟实现第三步


举个例子
bind 还有一个特点,就是

一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器。提供的
this 值被忽略,同时调用时的参数被提供给模拟函数。

也就是说当 bind 返回的函数作为构造函数的时候,bind 时指定的 this
值会失效,但传入的参数依然生效

        var value = 2;

        var foo = {
            value: 1
        };

        function bar(name, age) {
            this.habit = 'shopping';
            console.log(this.value);
            console.log(name);
            console.log(age);
        }

        bar.prototype.friend = 'kevin';

        var bindFoo = bar.bind(foo, 'daisy');

        var obj = new bindFoo('18');
        console.log(obj instanceof bar);

        // undefined
        // daisy
        // 18
        // true
        console.log(obj.habit);
        console.log(obj.friend);
        // shopping
        // kevin

注意:尽管在全局和 foo 中都声明了 value 值,最后依然返回了
undefind,说明绑定的 this 失效了,这时候 this 已经指向 bar 实例了。

所以我们可以通过修改返回的函数的原型来实现,让我们写一下:

        // 第三版
        Function.prototype.bind2 = function (context) {
            var self = this;  
            var args = Array.prototype.slice.call(arguments, 1);

            var fBound = function () {
                var bindArgs = Array.prototype.slice.call(arguments);
                // 当作为构造函数时,this 指向实例,此时结果为 true,将绑定函数的 this 指向该实例,可以让实例获得来自绑定函数的值
                // 以上面的是 demo 为例,如果改成 `this instanceof fBound ? null : context`,实例只是一个空对象,将 null 改成 this ,实例会具有 habit 属性
                // 当作为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context
                return self.apply(this instanceof fBound ? this : context, args.concat(bindArgs));
            }
            // 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值
            fBound.prototype = this.prototype;
            return fBound;
        }

注意:bind2 中的 this 与 fBound 中的 this 不一样,
前者是绑定函数对象,后者如果返回函数作为构造函数调用,那么 this
指向返回函数实例对象, 如果当作普通函数调用的话,this 指向 window。

模拟实现第三步

模拟代码已经完成 80%,还有两个小点要注意:

1.this 参数可以传 null,当为 null 的时候,视为指向 window

举个例子:

var value = 1; function bar() { console.log(this.value); }
bar.call(null); // 1

1
2
3
4
5
6
7
var value = 1;
 
function bar() {
    console.log(this.value);
}
 
bar.call(null); // 1

虽然这个例子本身不使用 call,结果依然一样。

2.函数是可以有返回值的!

举个例子:

var obj = { value: 1 } function bar(name, age) { return { value:
this.value, name: name, age: age } } console.log(bar.call(obj, ‘kevin’,
18)); // Object { // value: 1, // name: ‘kevin’, // age: 18 // }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var obj = {
    value: 1
}
 
function bar(name, age) {
    return {
        value: this.value,
        name: name,
        age: age
    }
}
 
console.log(bar.call(obj, ‘kevin’, 18));
// Object {
//    value: 1,
//    name: ‘kevin’,
//    age: 18
// }

不过都很好解决,让我们直接看第三版也就是最后一版的代码:

// 第三版 Function.prototype.call2 = function (context) { var context =
context || window; context.fn = this; var args = []; for(var i = 1,
len = arguments.length; i len; i++) { args.push(‘arguments[‘ + i +
‘]’); } var result = eval(‘context.fn(‘ + args +’)’); delete context.fn
return result; } // 测试一下 var value = 2; var obj = { value: 1 }
function bar(name, age) { console.log(this.value); return { value:
this.value, name: name, age: age } } bar.call(null); // 2
console.log(bar.call2(obj, ‘kevin’, 18)); // 1 // Object { // value: 1,
// name: ‘kevin’, // age: 18 // }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 第三版
Function.prototype.call2 = function (context) {
    var context = context || window;
    context.fn = this;
 
    var args = [];
    for(var i = 1, len = arguments.length; i  len; i++) {
        args.push(‘arguments[‘ + i + ‘]’);
    }
 
    var result = eval(‘context.fn(‘ + args +’)’);
 
    delete context.fn
    return result;
}
 
// 测试一下
var value = 2;
 
var obj = {
    value: 1
}
 
function bar(name, age) {
    console.log(this.value);
    return {
        value: this.value,
        name: name,
        age: age
    }
}
 
bar.call(null); // 2
 
console.log(bar.call2(obj, ‘kevin’, 18));
// 1
// Object {
//    value: 1,
//    name: ‘kevin’,
//    age: 18
// }

到此,我们完成了 call 的模拟实现,给自己一个赞 b( ̄▽ ̄)d

深入系列

JavaScript深入系列目录地址:。

JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。

本系列:

  1. JavaScirpt 深入之从原型到原型链
  2. JavaScript
    深入之词法作用域和动态作用域
  3. JavaScript 深入之执行上下文栈
  4. JavaScript 深入之变量对象
  5. JavaScript 深入之作用域链
  6. JavaScript 深入之从 ECMAScript 规范解读
    this
  7. JavaScript 深入之执行上下文
  8. JavaScript 深入之闭包
  9. JavaScript 深入之参数按值传递
  10. JavaScript
    深入之call和apply的模拟实现
  11. JavaScript 深入之bind的模拟实现

    1 赞 1 收藏
    评论

图片 1

构造函数效果的优化实现

但是在这个写法中,我们直接将 fbound.prototype =
this.prototype,我们直接修改 fbound.prototype 的时候,也会直接修改函数的
prototype。这个时候,我们可以通过一个空函数来进行中转:

// 第四版 Function.prototype.bind2 = function (context) { var self =
this; var args = Array.prototype.slice.call(arguments, 1); var fNOP =
function () {}; var fbound = function () { var bindArgs =
Array.prototype.slice.call(arguments); self.apply(this instanceof self ?
this : context, args.concat(bindArgs)); } fNOP.prototype =
this.prototype; fbound.prototype = new fNOP(); return fbound; }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 第四版
Function.prototype.bind2 = function (context) {
 
    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);
 
    var fNOP = function () {};
 
    var fbound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        self.apply(this instanceof self ? this : context, args.concat(bindArgs));
    }
    fNOP.prototype = this.prototype;
    fbound.prototype = new fNOP();
    return fbound;
 
}

到此为止,大的问题都已经解决,给自己一个赞!o( ̄▽ ̄)d

模拟实现第四步

但是在这个写法中,我们直接将 fBound.prototype =
this.prototype,我们直接修改 fBound.prototype
的时候,也会直接修改绑定函数的 prototype。
举例:

        Function.prototype.bind2 = function (context) {
            var self = this;
            var args = Array.prototype.slice.call(arguments, 1);

            var fBound = function () {
                var bindArgs = Array.prototype.slice.call(arguments);
                self.apply(this instanceof fBound ? this : context, args.concat(bindArgs));
            }
            fBound.prototype = this.prototype;
            return fBound;
        }


        function bar() {}

        var bindFoo = bar.bind2(null);

        bindFoo.prototype.value = 1;

        console.log(bar.prototype.value) // 1

这个时候,我们可以通过一个空函数来进行中转:

            // 第四版
        Function.prototype.bind2 = function (context) {

            var self = this;
            var args = Array.prototype.slice.call(arguments, 1);

            var fNOP = function () {};

            var fBound = function () {
                var bindArgs = Array.prototype.slice.call(arguments);
                return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
            }

            fNOP.prototype = this.prototype;
            fBound.prototype = new fNOP(); // 原型链继承
            return fBound;
        }


        function bar() {}

        bar.prototype.friend = 'kevin';
        bar.prototype.value = 2;

        var bindFoo = bar.bind2(null);

        bindFoo.prototype.value = 1;

        console.log(bar.prototype.value) // 2
        console.log(bindFoo.prototype.value);  // 1

        var obj = new bindFoo();
        console.log(obj.friend);  // kevin

apply的模拟实现

apply 的实现跟 call 类似,在这里直接给代码,代码来自于知乎 @郑航的实现:

Function.prototype.apply = function (context, arr) { var context =
Object(context) || window; context.fn = this; var result; if (!arr) {
result = context.fn(); } else { var args = []; for (var i = 0, len =
arr.length; i len; i++) { args.push(‘arr[‘ + i + ‘]’); } result =
eval(‘context.fn(‘ + args + ‘)’) } delete context.fn return result; }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Function.prototype.apply = function (context, arr) {
    var context = Object(context) || window;
    context.fn = this;
 
    var result;
    if (!arr) {
        result = context.fn();
    }
    else {
        var args = [];
        for (var i = 0, len = arr.length; i  len; i++) {
            args.push(‘arr[‘ + i + ‘]’);
        }
        result = eval(‘context.fn(‘ + args + ‘)’)
    }
 
    delete context.fn
    return result;
}

三个小问题

接下来处理些小问题:

1.apply 这段代码跟 MDN 上的稍有不同

在 MDN 中文版讲 bind 的模拟实现时,apply 这里的代码是:

self.apply(this instanceof self ? this : context || this,
args.concat(bindArgs))

1
self.apply(this instanceof self ? this : context || this, args.concat(bindArgs))

多了一个关于 context 是否存在的判断,然而这个是错误的!

举个例子:

var value = 2; var foo = { value: 1, bar: bar.bind(null) }; function
bar() { console.log(this.value); } foo.bar() // 2

1
2
3
4
5
6
7
8
9
10
11
var value = 2;
var foo = {
    value: 1,
    bar: bar.bind(null)
};
 
function bar() {
    console.log(this.value);
}
 
foo.bar() // 2

以上代码正常情况下会打印 2,如果换成了 context || this,这段代码就会打印
1!

所以这里不应该进行 context 的判断,大家查看 MDN
同样内容的英文版,就不存在这个判断!

2.调用 bind 的不是函数咋办?

不行,我们要报错!

if (typeof this !== “function”) { throw new
Error(“Function.prototype.bind – what is trying to be bound is not
callable”); }

1
2
3
if (typeof this !== "function") {
  throw new Error("Function.prototype.bind – what is trying to be bound is not callable");
}

3.我要在线上用

那别忘了做个兼容:

Function.prototype.bind = Function.prototype.bind || function () { …… };

1
2
3
Function.prototype.bind = Function.prototype.bind || function () {
    ……
};

当然最好是用es5-shim啦。

重要参考

知乎问题 不能使用call、apply、bind,如何用 js 实现 call 或者 apply
的功能?

最终代码

所以最最后的代码就是:

Function.prototype.bind2 = function (context) { if (typeof this !==
“function”) { throw new Error(“Function.prototype.bind – what is trying
to be bound is not callable”); } var self = this; var args =
Array.prototype.slice.call(arguments, 1); var fNOP = function () {}; var
fbound = function () { self.apply(this instanceof self ? this : context,
args.concat(Array.prototype.slice.call(arguments))); } fNOP.prototype =
this.prototype; fbound.prototype = new fNOP(); return fbound; }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Function.prototype.bind2 = function (context) {
 
    if (typeof this !== "function") {
      throw new Error("Function.prototype.bind – what is trying to be bound is not callable");
    }
 
    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);
    var fNOP = function () {};
 
    var fbound = function () {
        self.apply(this instanceof self ? this : context, args.concat(Array.prototype.slice.call(arguments)));
    }
 
    fNOP.prototype = this.prototype;
    fbound.prototype = new fNOP();
 
    return fbound;
 
}

深入系列

JavaScript深入系列目录地址:。

JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。

本系列:

  1. JavaScirpt 深入之从原型到原型链
  2. JavaScript
    深入之词法作用域和动态作用域
  3. JavaScript 深入之执行上下文栈
  4. JavaScript 深入之变量对象
  5. JavaScript 深入之作用域链
  6. JavaScript 深入之从 ECMAScript 规范解读
    this
  7. JavaScript 深入之执行上下文
  8. JavaScript 深入之闭包
  9. JavaScript 深入之参数按值传递

    1 赞 收藏
    评论

图片 1

深入系列

JavaScript深入系列目录地址:。

JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。

本系列:

  1. JavaScirpt 深入之从原型到原型链
  2. JavaScript
    深入之词法作用域和动态作用域
  3. JavaScript 深入之执行上下文栈
  4. JavaScript 深入之变量对象
  5. JavaScript 深入之作用域链
  6. JavaScript 深入之从 ECMAScript 规范解读
    this
  7. JavaScript 深入之执行上下文
  8. JavaScript 深入之闭包
  9. JavaScript 深入之参数按值传递
  10. JavaScript
    深入之call和apply的模拟实现

    1 赞 收藏
    评论

图片 1

发表评论

电子邮件地址不会被公开。 必填项已用*标注

网站地图xml地图