深入之bind的模拟实现,深入之call和apply的模拟实现

作者:金沙澳门官网手机版

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

2017/05/25 · JavaScript · apply, call

原文出处: 冴羽   

JavaScript 深入之bind的模拟实现

2017/05/26 · JavaScript · bind

原文出处: 冴羽   

  1. 实现一个new操作符

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 函数执行了

bind

一句话介绍 bind:

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

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

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

new操作符做了这些事:

模拟实现第一步

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

试想当调用 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 哎!是不是很开心!(~ ̄▽ ̄)~

返回函数的模拟实现

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

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);
    }
 
}

它创建了一个全新的对象。

模拟实现第二步

最一开始也讲了,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

(๑•̀ㅂ•́)و✧

传参的模拟实现

接下来看第二点,可以传入参数。这个就有点让人费解了,我在 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));
    }
 
}

它会被执行[[Prototype]](也就是__proto__)链接。

模拟实现第三步

模拟代码已经完成 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

构造函数效果的模拟实现

完成了这两点,最难的部分到啦!因为 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深入之从原型到原型链》。

它使this指向新创建的对象。。

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;
}

构造函数效果的优化实现

但是在这个写法中,我们直接将 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

通过new创建的每个对象将最终被[[Prototype]]链接到这个函数的prototype对象上。

重要参考

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

三个小问题

接下来处理些小问题:

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啦。

如果函数没有返回对象类型Object(包含Functoin, Array, Date, RegExg, Error),那么new表达式中的函数调用将返回该对象引用。function New {

深入系列

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

最终代码

所以最最后的代码就是:

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;
 
}

var res = {};

深入系列

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 赞 收藏 评论

图片 2

if (func.prototype !== null) {

res.__proto__ = func.prototype;

}

var ret = func.apply(res, Array.prototype.slice.call(arguments, 1));

if ((typeof ret === "object" || typeof ret === "function") && ret !== null) {

return ret;

}

return res;

}

var obj = New;

// equals to

var obj = new A;

  1. 实现一个JSON.stringifyJSON.stringify(value[, replacer [, space]]):

Boolean | Number| String 类型会自动转换成对应的原始值。

undefined、任意函数以及symbol,会被忽略(出现在非数组对象的属性值中时),或者被转换成 null。

不可枚举的属性会被忽略

如果一个对象的属性值通过某种间接的方式指回该对象本身,即循环引用,属性也会被忽略。function jsonStringify {

let type = typeof obj;

if (type !== "object") {

if (/string|undefined|function/.test {

obj = '"' + obj + '"';

}

return String;

} else {

let json = []

let arr = Array.isArray

for (let k in obj) {

let v = obj[k];

let type = typeof v;

if (/string|undefined|function/.test {

v = '"' + v + '"';

} else if (type === "object") {

v = jsonStringify;

}

json.push((arr ? "" : '"' + k + '":') + String;

}

return (arr ? "[" : "{") + String + (arr ? "]" : "}")

}

}

jsonStringify // "{"x":5}"

jsonStringify([1, "false", false]) // "[1,"false",false]"

jsonStringify({b: undefined}) // "{"b":"undefined"}"

  1. 实现一个JSON.parseJSON.parse(text[, reviver])

用来解析JSON字符串,构造由字符串描述的JavaScript值或对象。提供可选的reviver函数用以在返回之前对所得到的对象执行变换。

3.1 第一种:直接调用 evalfunction jsonParse {

return eval('(' + opt + ')');

}

jsonParse(jsonStringify

// Object { x: 5}

jsonParse(jsonStringify([1, "false", false]))

// [1, "false", falsr]

jsonParse(jsonStringify({b: undefined}))

// Object { b: "undefined"}

避免在不必要的情况下使用 eval,eval() 是一个危险的函数, 他执行的代码拥有着执行者的权利。如果你用 eval()运行的字符串代码被恶意方操控修改,您最终可能会在您的网页/扩展程序的权限下,在用户计算机上运行恶意代码。

它会执行JS代码,有XSS漏洞。

如果你只想记这个方法,就得对参数json做校验。var rx_one = /^[],:{}s]*$/;

var rx_two = /\(?:["\/bfnrt]|u[0-9a-fA-F]{4})/g;

var rx_three = /"[^"\nr]*"|true|false|null|-?d+?(?:[eE][+-]?d+)?/g;

var rx_four = /+/g;

if (

rx_one.test(

json

.replace(rx_two, "@")

.replace(rx_three, "]")

.replace(rx_four, "")

)

) {

var obj = eval("(" +json + ")");

}

3.2 第二种:Function来源 神奇的eval()与new Function()

核心:Function与eval有相同的字符串参数特性。var func = new Function(arg1, arg2, ..., functionBody);

在转换JSON的实际应用中,只需要这么做。var jsonStr = '{ "age": 20, "name": "jack" }'

var json = (new Function('return ' + jsonStr))();

复制代码

eval 与 Function 都有着动态编译js代码的作用,但是在实际的编程中并不推荐使用。

这里是面向面试编程,写这两种就够了。

  1. 实现一个call或 apply

call语法:fun.call(thisArg, arg1, arg2, ...),调用一个函数, 其具有一个指定的this值和分别地提供的参数。

apply语法:func.apply(thisArg, [argsArray]),调用一个函数,以及作为一个数组提供的参数。

4.1 Function.call按套路实现

call核心:

将函数设为对象的属性

执行&删除这个函数

指定this到函数并传入给定参数执行函数

如果不传入参数,默认指向为 window

为啥说是套路实现呢?因为真实面试中,面试官很喜欢让你逐步地往深考虑,这时候你可以反套路他,先写个简单版的:

4.1.1 简单版var foo = {

value: 1,

bar: function() {

console.log(this.value)

}

}

foo.bar() // 1

复制代码

4.1.2 完善版

当面试官有进一步的发问,或者此时你可以假装思考一下。然后写出以下版本:Function.prototype.call2 = function(content = window) {

content.fn = this;

let args = [...arguments].slice;

let result = content.fn;

delete content.fn;

return result;

}

let foo = {

value: 1

}

function bar(name, age) {

console.log

console.log

console.log(this.value);

}

bar.call2(foo, 'black', '18') // black 18 1

4.2 Function.apply的模拟实现

apply()的实现和call()类似,只是参数形式不同。直接贴代码吧:Function.prototype.apply2 = function(context = window) {

context.fn = this

let result;

// 判断是否有第二个参数

if(arguments[1]) {

result = context.fn(...arguments[1])

} else {

result = context.fn()

}

delete context.fn

return result

}

  1. 实现一个Function.bind()

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

此外,bind实现需要考虑实例化后对原型链的影响。Function.prototype.bind2 = function {

if(typeof this != "function") {

throw Error("not a function")

}

// 若没问参数类型则从这开始写

let fn = this;

let args = [...arguments].slice;

let resFn = function() {

return fn.apply(this instanceof resFn ? this : content,args.concat(...arguments) )

}

function tmp() {}

tmp.prototype = this.prototype;

resFn.prototype = new tmp();

return resFn;

}

  1. 实现一个继承

寄生组合式继承

一般只建议写这种,因为其它方式的继承会在一次实例中调用两次父类的构造函数或有其它缺点。

核心实现是:用一个 F 空的构造函数去取代执行了 Parent 这个构造函数。function Parent {

this.name = name;

}

Parent.prototype.sayName = function() {

console.log('parent name:', this.name);

}

function Child(name, parentName) {

Parent.call(this, parentName);

this.name = name;

}

function create {

function F(){}

F.prototype = proto;

return new F();

}

Child.prototype = create(Parent.prototype);

Child.prototype.sayName = function() {

console.log('child name:', this.name);

}

Child.prototype.constructor = Child;

var parent = new Parent;

parent.sayName(); // parent name: father

var child = new Child('son', 'father');

  1. 实现一个JS函数柯里化

图片 3

什么是柯里化?在计算机科学中,柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。

函数柯里化的主要作用和特点就是参数复用、提前返回和延迟执行。

7.1 通用版function curry {

var length = fn.length;

var args = args || [];

return function(){

newArgs = args.concat(Array.prototype.slice.call(arguments));

if (newArgs.length < length) {

return curry.call(this,fn,newArgs);

}else{

return fn.apply(this,newArgs);

}

}

}

function multiFn {

return a * b * c;

}

var multi = curry;

multi;

multi;

multi;

multi;

7.2 ES6骚写法const curry = (fn, arr = []) => => (

arg => arg.length === fn.length

? fn

: curry

)([...arr, ...args])

let curryTest=curry=>a+b+c+d)

curryTest //返回10

curryTest //返回10

curryTest //返回10

  1. 手写一个Promise

我们来过一遍Promise/A+规范:

三种状态pending| fulfilled | rejected

当处于pending状态的时候,可以转移到fulfilled或者rejected状态

当处于fulfilled状态或者rejected状态的时候,就不可变。必须有一个then异步执行方法,then接受两个参数且必须返回一个promise:// onFulfilled 用来接收promise成功的值

// onRejected 用来接收promise失败的原因

promise1=promise.then(onFulfilled, onRejected);

8.1 Promise的流程图分析

图片 4

来回顾下Promise用法:var promise = new Promise((resolve,reject) => {

if {

resolve

} else {

reject

}

})

promise.then(function {

// success

},function {

// failure

})

8.2 面试够用版function myPromise(constructor){

let self=this;

self.status="pending" //定义状态改变前的初始状态

self.value=undefined;//定义状态为resolved的时候的状态

self.reason=undefined;//定义状态为rejected的时候的状态

function resolve{

//两个==="pending",保证了状态的改变是不可逆的

if(self.status==="pending"){

self.value=value;

self.status="resolved";

}

}

function reject{

//两个==="pending",保证了状态的改变是不可逆的

if(self.status==="pending"){

self.reason=reason;

self.status="rejected";

}

}

//捕获构造异常

try{

constructor(resolve,reject);

}catch{

reject;

}

}

同时,需要在myPromise的原型上定义链式调用的then方法:myPromise.prototype.then=function(onFullfilled,onRejected){

let self=this;

switch(self.status){

case "resolved":

onFullfilled(self.value);

break;

case "rejected":

onRejected(self.reason);

break;

default:

}

}

测试一下:var p=new myPromise(function(resolve,reject){resolve;

p.then(function{console.log

//输出1

8.3 大厂专供版

直接贴出来吧,这个版本还算好理解const PENDING = "pending";

const FULFILLED = "fulfilled";

const REJECTED = "rejected";

function Promise {

let that = this; // 缓存当前promise实例对象

that.status = PENDING; // 初始状态

that.value = undefined; // fulfilled状态时 返回的信息

that.reason = undefined; // rejected状态时 拒绝的原因

that.onFulfilledCallbacks = []; // 存储fulfilled状态对应的onFulfilled函数

that.onRejectedCallbacks = []; // 存储rejected状态对应的onRejected函数

function resolve { // value成功态时接收的终值

if(value instanceof Promise) {

return value.then(resolve, reject);

}

// 实践中要确保 onFulfilled 和 onRejected 方法异步执行,且应该在 then 方法被调用的那一轮事件循环之后的新执行栈中执行。

setTimeout => {

// 调用resolve 回调对应onFulfilled函数

if (that.status === PENDING) {

// 只能由pending状态 => fulfilled状态 (避免调用多次resolve reject)

that.status = FULFILLED;

that.value = value;

that.onFulfilledCallbacks.forEach(cb => cb(that.value));

}

});

}

function reject { // reason失败态时接收的拒因

setTimeout => {

// 调用reject 回调对应onRejected函数

if (that.status === PENDING) {

// 只能由pending状态 => rejected状态 (避免调用多次resolve reject)

that.status = REJECTED;

that.reason = reason;

that.onRejectedCallbacks.forEach(cb => cb(that.reason));

}

});

}

// 捕获在excutor执行器中抛出的异常

// new Promise((resolve, reject) => {

// throw new Error('error in excutor')

// })

try {

excutor(resolve, reject);

} catch {

reject;

}

}

Promise.prototype.then = function(onFulfilled, onRejected) {

const that = this;

let newPromise;

// 处理参数默认值 保证参数后续能够继续执行

onFulfilled =

typeof onFulfilled === "function" ? onFulfilled : value => value;

onRejected =

typeof onRejected === "function" ? onRejected : reason => {

throw reason;

};

if (that.status === FULFILLED) { // 成功态

return newPromise = new Promise((resolve, reject) => {

setTimeout => {

try{

let x = onFulfilled(that.value);

resolvePromise(newPromise, x, resolve, reject); // 新的promise resolve 上一个onFulfilled的返回值

} catch {

reject; // 捕获前面onFulfilled中抛出的异常 then(onFulfilled, onRejected);

}

});

})

}

if (that.status === REJECTED) { // 失败态

return newPromise = new Promise((resolve, reject) => {

setTimeout => {

try {

let x = onRejected(that.reason);

resolvePromise(newPromise, x, resolve, reject);

} catch {

reject;

}

});

});

}

if (that.status === PENDING) { // 等待态

// 当异步调用resolve/rejected时 将onFulfilled/onRejected收集暂存到集合中

return newPromise = new Promise((resolve, reject) => {

that.onFulfilledCallbacks.push => {

try {

let x = onFulfilled;

resolvePromise(newPromise, x, resolve, reject);

} catch {

reject;

}

});

that.onRejectedCallbacks.push => {

try {

let x = onRejected;

resolvePromise(newPromise, x, resolve, reject);

} catch {

reject;

}

});

});

}

};

图片 5

emmm,我还是乖乖地写回进阶版吧。9. 手写防抖(Debouncing)和节流(Throttling)scroll 事件本身会触发页面的重新渲染,同时 scroll 事件的 handler 又会被高频度的触发, 因此事件的 handler 内部不应该有复杂操作,例如 DOM 操作就不应该放在事件处理中。 针对此类高频度触发事件问题(例如页面 scroll ,屏幕 resize,监听用户输入等),有两种常用的解决方法,防抖和节流。

9.1 防抖(Debouncing)实现

典型例子:限制 鼠标连击 触发。

一个比较好的解释是:当一次事件发生后,事件处理器要等一定阈值的时间,如果这段时间过去后 再也没有 事件发生,就处理最后一次发生的事件。假设还差 0.01 秒就到达指定时间,这时又来了一个事件,那么之前的等待作废,需要重新再等待指定时间。

图片 6

// 防抖动函数

function debounce(fn,wait=50,immediate) {

let timer;

return function() {

if(immediate) {

fn.apply(this,arguments)

}

if clearTimeout

timer = setTimeout=> {

fn.apply(this,arguments)

},wait)

}

}

结合实例:滚动防抖// 简单的防抖动函数

// 实际想绑定在 scroll 事件上的 handler

function realFunc(){

console.log("Success");

}

// 采用了防抖动

window.addEventListener('scroll',debounce(realFunc,500));

// 没采用防抖动

window.addEventListener('scroll',realFunc);

9.2 节流(Throttling)实现可以理解为事件在一个管道中传输,加上这个节流阀以后,事件的流速就会减慢。实际上这个函数的作用就是如此,它可以将一个函数的调用频率限制在一定阈值内,例如 1s,那么 1s 内这个函数一定不会被调用两次

图片 7

简单的节流函数:function throttle {

let prev = new Date();

return function() {

const args = arguments;

const now = new Date();

if (now - prev > wait) {

fn.apply(this, args);

prev = new Date();

}

}

复制代码

9.3 结合实践

通过第三个参数来切换模式。const throttle = function(fn, delay, isDebounce) {

let timer

let lastCall = 0

return function {

if (isDebounce) {

if clearTimeout

timer = setTimeout => {

fn

}, delay)

} else {

const now = new Date().getTime()

if (now - lastCall < delay) return

lastCall = now

fn

}

}

}

  1. 手写一

图片 8

个JS深拷贝

有个最著名的乞丐版实现,在《你不知道的JavaScript》里也有提及:

10.1 乞丐版 var newObj = JSON.parse( JSON.stringify( someObj ) );

10.2 面试够用版function deepCopy{

//判断是否是简单数据类型,

if(typeof obj == "object"){

//复杂数据类型

var result = obj.constructor == Array ? [] : {};

for(let i in obj){

result[i] = typeof obj[i] == "object" ? deepCopy : obj[i];

}

}else {

//简单数据类型 直接 == 赋值

var result = obj;

}

return result;

}

关于深拷贝的讨论天天有,这里就贴两种吧,毕竟我...

图片 9

11.实现一个instanceOffunction instanceOf(left,right) {

let proto = left.__proto__;

let prototype = right.prototype

while {

if(proto === null) return false

if(proto === prototype) return true

proto = proto.__proto__;

}

}

图片 10

本文由金沙手机娱乐网址发布,转载请注明来源

关键词: