原稿:语雀 · 原生重写 · 原目录:手撕代码 › 原生重写

instanceof

对于A.instanceof(B),它可以判断A的原型链上是否有B的原型

// left的原型链上有right.prototype
function instance_of(left, right) {
  // 父类的原型对象
  let rightProto = right.prototype;
  // 子类的隐式原型
  let leftValue = left.__proto__;
  while (true) {
    if (leftValue === null) return false;
    if (leftValue === rightProto) return true;
    // 沿原型链上溯
    leftValue = leftValue.__proto__;
  }
}

// 测试用例
class Animal {
  constructor() {
    this.name = "animal";
    this.sex = "male";
  }

  sleep() {
    console.log("animals can sleep");
  }
}

class Dog extends Animal {
  constructor() {
    super();
    this.age = 0;
  }
  bark = function () {
    console.log("dogs can bark");
  };
}

new

new用来配合构造函数创建对象,当使用new时会发生: 2. 在内存中创建新对象 4. 新对象内部的

\[Prototype\[Prototype

]指向构造函数的原型对象 6. 构造函数内部的this指向新对象 8. 执行构造函数内部代码 10. 如果构造函数返回非空对象,则返回该对象,否则返回第1步创建的对象

function objectFactory(constructor, ...args) {
  let obj = new Object();
  obj.__proto__ = constructor.prototype;
  let ret = constructor.apply(obj, args);
  if(ret && (typeof ret == "object" || typeof ret == "function")) return ret;
  return obj;
}

// 测试用例
function Animal(name, sex) {
  this.name = name;
  this.sex = sex;
}

let animal = objectFactory(Animal, "Eve", "female");
console.log(animal);

call()

call和apply类似,由一个函数调用,执行该函数,但是执行上下文中的this指向了我们传入的指定对象,为了实现call(),我们需要 2. 把函数新建为传入对象的一个属性,当传入null时,对象为window 4. 使用对象执行函数 6. 删除刚才添加的属性 8. 返回

不能使用箭头函数=(context)=》,因为执行时没有this和arguments,其this取决于定义位置上下文中this

Function.prototype.myCall = function (context) {
  context = context || window;
  context.fn = this;

  // 这里毕竟还在实现 call,不要使用 [].slice.call(arguments,1)
  let args = [...arguments].slice(1);
  let result = 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.myCall(null);
console.log(bar.myCall(obj, "kevin", 18));

参考链接:JavaScript深入之call和apply的模拟实现


apply()

比起call还要简单,可以直接展开arguments

11

,注意无参数情况

Function.prototype.myApply = function (context) {
  context = context || window;
  context.fn = this;

  // 注意无参数情况
  let result = (arguments[1]) ? context.fn(...arguments[1]) : context.fn();
  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.myApply(null);
console.log(bar.myApply(obj, ["kevin", 18]));

bind()

根据MDN:Function.prototype.bind(),bind()中如果传入的context为null时,执行作用域的 this 将被视为新函数的 thisArg,而不是想call()/apply()绑定到全局对象

bind的功能

  • 传入指定对象作为函数执行的this,可传入预置参数
  • 返回一个函数对象

实现要点

  • 当调用bind的不是函数时,需要抛出异常
  • 在bind阶段可传入预置参数,执行返回函数时可以继续传参,两部分参数拼接
  • 返回函数可作为构造函数使用,此时之前绑定的this失效
  • 返回的函数对象可访问绑定函数的原型属性,但返回函数原型属性改动不应该同步到绑定函数
Function.prototype.myBind = function (context) {  if (typeof this !== "function") {    throw new TypeError(      "Function.prototype.bind - what is trying to be bound is not callable"    );  }  // 要被绑定的函数  let fToBind = this,    // 预置参数    args1 = [].slice.call(arguments, 1),    // 空函数(保存原型快照)    fNOP = function () {},    // 返回函数    fBound = function () {      // 调用时传参,和预置参数拼接      let args2 = [...arguments];      // 如果(this instanceof fBound)成立,则为构造函数      return fToBind.apply(        this instanceof fBound ? this : context,        args1.concat(args2)      );    };  // 维护原型关系  fNOP.prototype = this.prototype;  fBound.prototype = new fNOP();  return fBound;};// 测试var value = 2;var foo = {  value: 1,};

参考链接:JavaScript深入之bind的模拟实现


继承

我根据红宝书总结了ES5的6种继承+ES6类继承,面试重点“组合式继承”、“寄生式组合继承”、“类继承”

JS继承 红宝书第八章写的真的很不错,第四版更新了ES6的继承,这里总结下✍ES5在ES5中没有正式的class,所有的类都是用于创建特定类型对象的构造函数,同时有以下事实JS中每个函数都具有一个prototype属性,该属性是一个指向函数原型对象的引用原型对象会获得constructor属性,该属性是… Front-end Baby👶

  • 组合式继承
// 父类
function Animal() {
  this.superName = "animal";
}
Animal.prototype.getSuperName = function () {
  return this.superName;
};
// 子类
function Dog() {
  // 借用构造函数
  Animal.call(this);
  this.subName = "dog";
}
// 原型链继承
Dog.prototype = new Animal();

// 测试
let dog = new Dog();
console.log(dog.getSuperName());
  • 寄生式组合继承
function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

function inheritPrototype(subType, superType){
  let prototype = object(superType.prototype);
  prototype.constructor = subType;
  subType.prototype = prototype;
}

function Animal() {
  this.superName = "animal";
}
Animal.prototype.getSuperName = function () {
  return this.superName;
};

function Dog() {
  Animal.call(this);
  this.subName = "dog";
}

// 寄生式继承
inheritPrototype(Dog, Animal);

let dog = new Dog();
console.log(dog.getSuperName());
  • 类继承
class Animal {
  constructor() {
    this.superName = "animal";
  }
  getSuperName() {
    return this.superName;
  }
}

// 别漏掉extends和super()
class Dog extends Animal {
  constructor() {
    super();
    this.subName = "dog";
  }
}

let dog = new Dog();
console.log(dog.getSuperName());

Object.create()

该方法实际上就是“原型式继承”中的临时构造函数object()——接受一个对象并返回以此为原型的实例,这种继承模式可以不用创建父类,直接利用已有实例作为模板,缺点是子类实例共享原型(已有实例)属性

function object(o){
  function F();
  F.prototype = o;
  return new F();
}

// 测试用例
// 已有实例
let bus = {
  name: "bus",
  colors: ["red","blue"]
};

let bus1 = object(bus);
bus1.colors.push("green");
let bus2 = object(bus);
bus2.colors.push("black");

console.log(bus2.colors); //["red", "blue", "green", "black"]

reduce()

reduce()是Array提供的归并方法,方法接收两个参数

  • 对每一项都会运行的归并函数和归并初始值
  • 对于归并函数,可接受四个参数:上一个归并值、当前项、当前项的索引和数组本身

如果没有提供归并初始值,则第一次执行时默认上一个归并值为数组首元素,当前项为数组第二个元素

方法最后返回一个归并运算的结果