1、实现instanceof运算符

instanceof运算符用于检测构造函数的 prototype属性是否出现在某个实例对象的原型链上,运算符左侧是实例对象,右侧是构造函数。

const iInstanceof = function(left, right) {
  let proto = Object.getPrototypeOf(left);
  while (true) {
    if (proto === null) return false;
    if (proto === right.prototype) return true;
    proto = Object.getPrototypeOf(proto);
  }
};

这是常见的实现,我们也可以用 isPrototypeOf实现

const iInstanceof = function (left, right) {
  return right.prototype.isPrototypeOf(left)
};

2、实现new操作符

new执行过程如下:

  1. 创建一个新对象;

  2. 新对象的[[prototype]]特性指向构造函数的prototype属性;

  3. 构造函数内部的this指向新对象;

  4. 执行构造函数;

  5. 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象;

const iNew = function (fn, ...rest) {
  let instance = Object.create(fn.prototype);
    let res = fn.apply(instance, rest);
    return res !== null && (typeof res === 'object' || typeof res === 'function') ? res : instance;
};

3、实现bind方法

改变函数内 this 的值并且传参,返回一个函数

const iBind = function(thisArg, ...args) {
  const originFunc = this;
  const boundFunc = function(...args1) {
    // 解决 bind 之后对返回函数 new 的问题
    if (new.target) {
      if (originFunc.prototype) {
        boundFunc.prototype = originFunc.prototype;
      }
      const res = originFunc.apply(this, args.concat(args1));
      return res !== null && (typeof res === 'object' || typeof res === 'function') ? res : this;
    } else {
      return originFunc.apply(thisArg, args.concat(args1));
    }
  };
  // 解决length 和 name 属性问题
  const desc = Object.getOwnPropertyDescriptors(originFunc);
  Object.defineProperties(boundFunc, {
    length: Object.assign(desc.length, {
      value: desc.length < args.length ? 0 : (desc.length - args.length)
    }),
    name: Object.assign(desc.name, {
      value: `bound ${desc.name.value}`
    })
  });
  return boundFunc;
};
// 保持 bind 的数据属性一致
Object.defineProperty(Function.prototype, 'iBind', {
  value: iBind,
  enumerable: false,
  configurable: true,
  writable: true
});

实现函数的 bind 方法核心是利用 call 绑定 this 指向,同时考虑了一些其他情况,例如

  • bind 返回的函数被 new 调用作为构造函数时,绑定的值会失效并且改为 new 指定的对象

  • 定义了绑定后函数的 length 属性和 name 属性(不可枚举属性)

  • 绑定后函数的 prototype 需指向原函数的 prototype(真实情况中绑定后的函数是没有 prototype 的,取而代之在绑定后的函数中有个 内部属性 [[TargetFunction]] 保存原函数,当将绑定后函数作为构造函数时,将创建的实例的 __proto__ 指向 [[TargetFunction]] 的 prototype,这里无法模拟内部属性,所以直接声明了一个 prototype 属性)

4、实现call方法

用指定的 this 值和参数来调用函数

const iCall = function(thisArg, ...args) {
  thisArg = (thisArg === undefined || thisArg === null) ? window : Object(thisArg);
  let fn = Symbol('fn');
  thisArg[fn] = this;
  let res = thisArg[fn](...args);
  delete thisArg[fn];
  return res;
};
// 保持 call 的数据属性一致
Object.defineProperty(Function.prototype, 'iCall', {
  value: iCall,
  configurable: true,
  enumerable: false,
  writable: true
});

原理就是将函数作为传入的上下文参数(context)的属性执行,这里为了防止属性冲突使用了 ES6 的 Symbol 类型

5、函数柯里化

将一个多参数函数转化为多个嵌套的单参数函数。

const curry = function(targetFn) {
  return function fn(...rest) {
    if (targetFn.length === rest.length) {
      return targetFn.apply(null, rest);
    } else {
      return fn.bind(null, ...rest);
    }
  };
};
// 用法
function add(a, b, c, d) {
  return a + b + c + d;
}
console.log('柯里化:', curry(add)(1)(2)(3)(4));
// 柯里化:10

6、发布订阅

class EventBus {
  constructor() {
    Object.defineProperty(this, 'handles', {
      value: {}
    });
  }
  on(eventName, listener) {
    if (typeof listener !== 'function') {
      console.error('请传入正确的回调函数');
      return;
    }
    if (!this.handles[eventName]) {
      this.handles[eventName] = [];
    }
    this.handles[eventName].push(listener);
  }
  emit(eventName, ...args) {
    let listeners = this.handles[eventName];
    if (!listeners) {
      console.warn(`${eventName}事件不存在`);
      return;
    }
    for (const listener of listeners) {
      listener(...args);
    }
  }
  off(eventName, listener) {
    if (!listener) {
      delete this.handles[eventName];
      return;
    }
    let listeners = this.handles[eventName];
    if (listeners && listeners.length) {
      let index = listeners.findIndex(item => item === listener);
      listeners.splice(index, 1);
    }
  }
  once(eventName, listener) {
    if (typeof listener !== 'function') {
      console.error('请传入正确的回调函数');
      return;
    }
    const onceListener = (...args) => {
      listener(...args);
      this.off(eventName, listener);
    };
    this.on(eventName, onceListener);
  }
}

7、深拷贝

const deepClone = function(source) {
  if (source === null || typeof source !== 'object') {
    return source;
  }
  let res = Array.isArray(source) ? [] : {};
  for (const key in source) {
    if (source.hasOwnProperty(key)) {
      res[key] = deepClone(source[key]);
    }
  }
  return res;
};

这个是深拷贝的很基础版本,其中存在一些问题,比如循环引用,比如递归爆栈,后面我会专门写一篇文章来展开讨论。

8、实现ES6的Class

用构造函数模拟,class 只能用 new 创建,不可以直接调用,另外注意一下属性的描述符

const checkNew = function(instance, con) {
  if (!(instance instanceof con)) {
    throw new TypeError(`Class constructor ${con.name} cannot be invoked without 'new'`);
  }
};
const defineProperties = function(target, obj) {
  for (const key in obj) {
    Object.defineProperty(target, key, {
      configurable: true,
      enumerable: false,
      value: obj[key],
      writable: true
    });
  }
};
const createClass = function(con, proto, staticAttr) {
  proto && defineProperties(con.prototype, proto);
  staticAttr && defineProperties(con, staticAttr);
  return con;
};
// 用法
function Person(name) {
  checkNew(this, Person);
  this.name = name;
}
var PersonClass = createClass(Person, {
  getName: function() {
    return this.name;
  }
}, {
  getAge: function() {}
});

9、实现ES6的继承

ES6 内部使用寄生组合式继承,首先用 Object.create 继承原型,并传递第二个参数以将父类构造函数指向自身,同时设置数据属性描述符。

然后用 Object.setPrototypeOf 继承静态属性和静态方法。

const inherit = function(subType, superType) {
  // 对 superType 进行类型判断
  if (typeof superType !== "function" && superType !== null) {
    throw new TypeError("Super expression must either be null or a function");
  }
  subType.prototype = Object.create(superType && superType.prototype, {
    constructor: {
      configurable: true,
      enumerable: false,
      value: subType,
      writable: true
    }
  });
  // 继承静态方法
  superType && Object.setPrototypeOf(subType, superType);
};
// 用法
function superType(name) {
  this.name = name;
}
superType.staticFn = function() {
  console.log('staticFn');
}
superType.prototype.getName = function() {
  console.log('name: ' + this.name);
}

function subType(name, age) {
  superType.call(this, name);
  this.age = age;
}
inherit(subType, superType);
// 必须在继承之后再往 subType 中添加原型方法,否则会被覆盖掉
subType.prototype.getAge = function() {
  console.log('age: ' + this.age);
}
let subTypeInstance = new subType('Twittytop', 29);
subType.staticFn();
subTypeInstance.getName();
subTypeInstance.getAge();

10、使用reduce实现数组flat方法

const selfFlat = function (depth = 1) {
  let arr = Array.prototype.slice.call(this)
  if (depth === 0 ) return arr
  return arr.reduce((pre, cur) => {
    if (Array.isArray(cur)) {
      return [...pre, ...selfFlat.call(cur, depth - 1)]
    } else {
      return [...pre, cur]
    }
  }, [])
}

因为 selfFlat 是依赖 this 指向的,所以在 reduce 遍历时需要指定 selfFlat 的 this 指向,否则会默认指向 window 从而发生错误

原理通过 reduce 遍历数组,遇到数组的某个元素仍是数组时,通过 ES6 的扩展运算符对其进行降维(ES5 可以使用 concat 方法),而这个数组元素可能内部还嵌套数组,所以需要递归调用 selfFlat

同时原生的 flat 方法支持一个 depth 参数表示降维的深度,默认为 即给数组降一层维度

传入 Inifity 会将传入的数组变成一个一维数组

11、CO(协成)实现

function co(gen) {
  return new Promise(function(resolve, reject) {
    if (typeof gen === 'function') gen = gen();
    if (!gen || typeof gen.next !== 'function') return resolve(gen);
    onFulfilled();
    function onFulfilled(res) {
      let ret;
      try {
        ret = gen.next(res);
      } catch (e) {
        return reject(e);
      }
      next(ret);
    }
    function onRejected(err) {
      let ret;
      try {
        ret = gen.throw(err);
      } catch (e) {
        return reject(e);
      }
      next(ret);
    }
    function next(ret) {
      if (ret.done) return resolve(ret.value);
      let val = Promise.resolve(ret.value);
      return val.then(onFulfilled, onRejected);
    }
  });
}

使用方法:

// 用法
co(function*() {
  let res1 = yield Promise.resolve(1);
  console.log(res1);
  let res2 = yield Promise.resolve(2);
  console.log(res2);
  let res3 = yield Promise.resolve(3);
  console.log(res3);
  return res1 + res2 + res3;
}).then(value => {
  console.log('add: ' + value);
}, function(err) {
  console.error(err.stack);
});

co 接受一个生成器函数,当遇到 yield 时就暂停执行,交出控制权,当其他程序执行完毕后,将结果返回并从中断的地方继续执行,如此往复,一直到所有的任务都执行完毕,最后返回一个 Promise 并将生成器函数的返回值作为 resolve 值。

我们将 * 换成 async,将 yield 换成 await 时,就和我们经常用的 async/await 是一样的,所以说 async/await 是生成器函数的语法糖。