1.手写 call、apply 及 bind 函数

首先要从以下几点来考虑如何实现这几个函数:
不传入第一个参数,那么上下文默认为 window
改变了 this 指向,让新的对象可以执行该函数,并能接受参数

window.x = 100;

(1)实现 call 函数

Function.prototype.myCall = function(context) {
    if (typeof this !== 'function') throw Error('this not a function');
    context = context || window;
    context.fn = this;
    // var args = [].slice.call(arguments).slice(1);
    // var result = context.fn(...args); // ES6

    // ES3
    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;
};
// 测试一下
function sum(a, b) {
    this.a = a;
    this.b = b;
    return this.x + a + b;
}
var sum1 = sum.myCall(null, 1, 2);
var sum2 = sum.myCall({ x: 1 }, 1, 2);
console.log(sum1, sum2); // 103  4

以下是对实现的分析:

  • 首先 context 为可选参数,如果不传的话默认上下文为 window
  • 接下来给 context 创建一个 fn 属性,并将值设置为需要调用的函数
  • 因为 call 可以传入多个参数作为调用函数的参数,所以需要将参数剥离出来
  • 然后调用函数并将对象上的函数删除

(2)实现 apply 函数

Function.prototype.myApply = function (context) {
    if (typeof this !== 'function') throw Error('this not a function');
    context = context || window;
    context.fn = this;
    // 处理参数和 call 有区别
    var result = arguments[1] ? context.fn(arguments[1]) : context.fn();
    delete context.fn;
    return result;
};
// 测试一下
function count(arr) {
    var sum = 0;
    for (var i = 0, len = arr.length; i < len; i++) {
        sum += arr[i];
    }
    return this.x + sum;
}
let count1 = count.myApply(null, [3, 4]);
let count2 = count.myApply({ x: 1 }, [3, 4]);
console.log(count1, count2); // 107  8

apply 的实现类似于 call,区别在于对参数的处理,所以就不一一分析思路了

(3)实现 bind 函数
bind 的实现对比其他两个函数的实现稍微复杂了一点,因为 bind 需要返回一个函数,需要判断一些边界问题,以下是 bind 的实现

Function.prototype.myBind = function (context) {
    if (typeof this !== 'function') throw Error('this not a function');
    var _this = this;
    var args = [].slice.call(arguments).slice(1);
    return function F() {
        if (this instanceof F) {
            return new _this(...args, ...arguments);
        }
        return _this.apply(context, args.concat(...arguments));
    }
};
// 测试一下
function mySum(a, b) {
    this.a = a;
    this.b = b;
    return this.x + a + b;
}
let mySum1 = mySum.myBind({ x: 1 });
console.log(mySum1(1, 2)); // 4
console.log(mySum1([1, 2])); // 4
let mySum2 = mySum.myBind({ x: 10 }, 1);
console.log(mySum2(2)); // 13
let mySum3 = mySum.myBind({ x: 1 });
let obj1 = new mySum3(1, 2);
console.log(obj1); // {a: 1, b: 2}

以下是对实现的分析:

  • 前几步和之前的实现差不多,就不赘述了
  • bind 返回了一个函数,对于函数来说有两种方式调用,一种是直接调用,一种是通过 new 的方式,我们先来说直接调用的方式
  • 对于直接调用来说,这里选择了 apply 的方式实现,但是对于参数需要注意以下情况:因为 bind 可以实现类似这样的代码 f.bind(obj, 1)(2),所以我们需要将两边的参数拼接起来,于是就有了这样的实现 args.concat(...arguments)
  • 最后来说通过 new 的方式,对于 new 的情况来说,不会被任何方式改变 this,所以对于这种情况我们需要忽略传入的 this

2.new 运算符原理以及实现

在调用 new 的过程中会发生以下四件事情:

  1. 新生成了一个对象
  2. 链接到原型
  3. 绑定 this
  4. 返回新对象

根据以上几个过程,我们也可以试着来自己实现一个 new

// 先创建一个构造函数 M 和对应的实例 o1,下面会用于测试我们手写的 myNew 函数
var M = function (name) {
    this.name = name;
};
var o1 = new M('o1');
var myNew = function (func) {
    // 一个新对象被创建,它继承自 func.prototype
    var o = Object.create(func.prototype);
    // 构造函数foo被执行。执行的时候,相应的参数会被传入,同时上下文(this)会被指定为这个新实例
    // new func 等同于 new func(),只能用在不传递任何参数的情况
    var k = func.call(o);
    if (typeof k === 'object') {
        // 如果构造函数返回了一个对象,那么这个对象会取代整个new出来的结果
        return k;
    } else {
        // 如果构造函数没有返回对象,那么 new 出来的结果就是之前 Object.create 创建的对象
        return o;
    }
};
var o2 = myNew(M);
console.log(o2 instanceof M); // true
console.log(o2 instanceof Object); // true
console.log(o2.__proto__.constructor === M); // true
console.log(o2.__proto__.constructor === Object); // false
M.prototype.walk = function () {
    console.log('walk');
};
o2.walk(); // 'walk'
o1.walk(); // 'walk'

实现分析:

  • 创建一个空对象,它继承自构造函数的原型
  • 绑定 this 并执行构造函数
  • 确保返回值为对象

3.instanceof 原理以及实现

instanceof 可以正确的判断对象的类型,因为内部机制是通过判断 对象的原型链 中是不是能找到 类型的 prototype。

function myInstanceof(left, right) {
    var prototype = right.prototype;
    left = left.__proto__;
    while (true) {
        if (left === null || left === undefined) return false;
        if (left === prototype) return true;
        left = left.__proto__;
    }
}
console.log(myInstanceof(o1, M)); // true
console.log(myInstanceof(o1, Object)); // true
console.log(myInstanceof([1, 2], Array)); // true
console.log(myInstanceof({ a: 1 }, Array)); // false

实现分析:

  • 首先获取右边参数 类型的显式原型
  • 然后获得左边参数 对象的隐式原型
  • 然后一直循环判断 对象的隐式原型 是否等于 类型的显式原型,直到 对象的隐式原型为 null,因为原型链最终为 null

lxcan
337 声望32 粉丝