關於原型、原型鏈、和繼承

彷佛生活中經常會遇到這種狀況,你去一家公司面試,前面面的都挺好,你以爲你對基礎算法的瞭解很好,各類排序,紅黑樹,二叉樹,深度/廣度優先算法都答出來了,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);            // 能夠對比一下,看看二者區別
相關文章
相關標籤/搜索