面試題之原型、原型鏈、和繼承

gentlecoder發表於2019-03-31

似乎生活中常常會遇到這種情況,你去一家公司面試,前面面的都挺好,你覺得你對基礎演算法的瞭解很好,各種排序,紅黑樹,二叉樹,深度/廣度優先演算法都答出來了,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);
複製程式碼

可以先猜一下是什麼結果。。

好吧,不用猜了。結果如下

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();  // 報錯,父類中沒有該方法
複製程式碼

這裡有三個注意點:

  • 父類中不會存在子類的方法(物件導向六大原則還記得嗎,開閉,單一職責,依賴倒轉,里氏置換,知道最少,介面隔離,合成聚合複用)
  • 上面程式碼中,talk方法中的this,預設指向Student類的例項。但是,如果將這個方法提取出來單獨使用,this會指向該方法執行時所在的環境(由於 class 內部是嚴格模式,所以 this 實際指向的是undefined),從而導致找不到info方法而報錯。解決辦法如程式碼中所示。
  • 關於箭頭函式的this指向問題,眾所周知,普通函式的this是指向呼叫它的那個物件,所以普通函式可以通過apply,call,bind來改變this的指向。而箭頭函式中的this是在定義函式的時候就已經確定了,指向外層作用域鏈中的普通函式,若沒有,this則指向undefined。

PS

(那麼問題又來了,手寫個apply,call,bind的polyfill吧)

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 真 棒
複製程式碼

拿走,不謝。

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);	// 可以對比一下,看看兩者區別
複製程式碼

相關文章