关于原型、原型链、和继承

彷佛生活中经常会遇到这种状况,你去一家公司面试,前面面的都挺好,你以为你对基础算法的了解很好,各类排序,红黑树,二叉树,深度/广度优先算法都答出来了,leetcode上的若干困难题目也都答上来了,而后面试官说,"那么好吧,介绍一下你对原型的见解吧。"

???我头发。我leetcode上刷了100天,我费劲心思研究了各类算法和数据结构,你叫我讲讲原型?面试

为了应对这种状况,本文经过以下代码展现下js的原型仅供参考。算法

上CODE

const c = (...v) => console.log(...v);
function People(name, age) {
  this.name = name;
  this.age = age;
}
/*你想这么写也不要紧
const People = function(name, age) {
  this.name = name;
  this.age = age;
}
*/
People.prototype.info = function() {
  c(`My name is ${this.name}, my age is ${this.age}.`);
};  // 在原型上定义方法
function Student(name, age, school) {
  People.call(this, ...arguments); 
  this.school = school;
}
Student.prototype = People.prototype;  
// 这里推荐用new People(),直接指定People.prototype会污染People原型,以下结果
Student.prototype.constructor = Student;  //修正constructor指向
Student.prototype.talk = function() {
  c(`My school is ${this.school}`);
};  // 须要在改变了Student的原型后定义,不然没法获取到该方法

const xiaoD = new Student("xiaoD", 4, "小星星幼儿园");
xiaoD.info();
xiaoD.talk();
const somebody = new People("somebody", 22);
somebody.talk();
c(xiaoD.__proto__ === Student.prototype);
c(Student.__proto__ === Function.prototype);
c(Function.prototype === Function.__proto__);
c(Function.__proto__ === Object.__proto__);
c(Object.__proto__ === Function.prototype);
c(Object.prototype === Function.prototype.__proto__);
c(Object.prototype.__proto__ === null);

能够先猜一下是什么结果。。数据结构

好吧,不用猜了。结果以下app

My name is xiaoD, my age is 4.
My school is 小星星幼儿园
My school is undefined
true
true
true
true
true
true
true

每一个实例对象( object )都有一个私有属性(称之为 proto )指向它的原型对象( prototype )。该原型对象也有一个本身的原型对象( proto ) ,层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并做为这个原型链中的最后一个环节。至于谁指谁,应该从上面的代码中就能够清晰的看出来了。这里注意的是只有函数中才有 prototype 属性。函数

类的继承

class People {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  info() {
    c(`My name is ${this.name}, my age is ${this.age}.`);
  }
}
class Student extends People {
  constructor(name, age, school) {
    super(...arguments);        // 继承属性
    this.school = school;
    this.talk = this.talk.bind(this);        // 绑定this
    /* 或者这样绑定
      this.talk = () => {
      this.info();  // 箭头函数中的this在定义时绑定
      c(`My school is ${this.school}`);
    };
      */
  }
  talk() {
    this.info();
    c(`My school is ${this.school}`);
  }
  
}

const xiaoD = new Student("xiaoD", 4, "小星星幼儿园");
xiaoD.talk();
const { talk } = xiaoD;
talk();        // 不绑定this这里会报错
const somebody = new People("somebody", 22);
somebody.talk();  // 报错,父类中没有该方法

这里有三个注意点:this

  • 父类中不会存在子类的方法(面向对象六大原则还记得吗,开闭,单一职责,依赖倒转,里氏置换,知道最少,接口隔离,合成聚合复用)
  • 上面代码中,talk方法中的this,默认指向Student类的实例。可是,若是将这个方法提取出来单独使用,this会指向该方法运行时所在的环境(因为 class 内部是严格模式,因此 this 实际指向的是undefined),从而致使找不到info方法而报错。解决办法如代码中所示。
  • 关于箭头函数的this指向问题,众所周知,普通函数的this是指向调用它的那个对象,因此普通函数能够经过apply,call,bind来改变this的指向。而箭头函数中的this是在定义函数的时候就已经肯定了,指向外层做用域链中的普通函数,若没有,this则指向undefined。

PS

(那么问题又来了,手写个apply,call,bind的polyfill吧)prototype

const obj = {
  name: "xiaoD"
};
const fn = function(...args) {
  c(this.name, ...args);
};
Function.prototype.myApply = function(obj, [...args]) {
  return this.call(obj, ...args);
};
fn.myApply(obj, ["真", "棒"]);        // xiaoD 真 棒
fn.apply(obj, ["真", "棒"]);            // xiaoD 真 棒

拿走,不谢。code

Function.prototype.myApply = function(obj, [...args]) {
  obj.fn = this;
  let ret = obj.fn(...args);
  delete obj.fn;
  return ret;
}; 
Function.prototype.myBind = function(obj) {
  obj.fn = this;
  return function(...args) {
    return obj.fn(...args);
  };
};
// 偷偷贴在这里

再PS

知道了原型、原型链,那new一个对象的过程知道吗,能手写一个吗。对象

new一个对象的过程大概分红三步:排序

  • 新建一个空对象
  • 改变原型链指向,调用构造函数
  • 返回这个新对象
const myNew = function(fn, ...args) {
  let obj = {};
  obj.__proto__ = fn.prototype;
  let ret = fn.call(obj, ...args);
  return ret ? ret : obj;
};
function People(name, age) {
  this.name = name;
  this.age = age;
}
let xiaoD = myNew(People, "xiaoD", 4);
let xiaoY = new People("xiaoY", 4);            // 能够对比一下,看看二者区别