es6之后,真的不需要知道原型鏈了嗎?


 

3月份幾乎每天都能看到面試的人從我身邊經過,前段時間同事聊面試話題提到了原型鏈,頓時激起了我在開始學習前端時很多心酸的回憶。第一次接觸js的面向對象思想是在讀《js高程設計》(紅寶書)的時候,這部分內容卡了我整整一個多月,記得那會兒用了很笨的辦法,我把這兩個章節來回讀了一遍又一遍,仍然不能完全理解,大部分是憑借機械記憶。因為入門的時候很喜歡紅寶書,在差不多一年的自學時間里基礎部分翻了將近10遍。當然,原型鏈也讀了10遍。很遺憾,那會兒我覺得自己只掌握了50%。直到讀了一個系列的書叫 《你不知道的javascript》,這本書神奇的叩開了我通往js學習之路的另一扇大門,簡直顛覆了我對js之前的所有認識。尤其是上卷關於this、閉包、原型鏈繼承的理解思想潛移默化的影響了我對這門語言的認知。我還記得這本書是我在北京的地鐵里用kindle讀完的,然后在博客里寫了4篇讀書筆記。對於原型鏈,我曾經很偏執的喜歡,后來在決定要轉前端之后到杭州的一次面試,因為面試是在周末,跟一家做人工智能的公司技術負責人聊了將近兩個小時,他給了我很多前端職業發展的中肯建議(初到杭州面試的那段時間真的得到了很多陌生人的指引跟幫助),糾正了我很多偏見的認知,至今我還記得他的花名。

原型鏈設計機制一直是大多數前端開發最難理解的部分,據說當初 Brendan Eich 設計之初不想引入類的概念,但是為了將對象聯系起來,加入的C++ new的概念,但是new沒有辦法共享屬性,就在構造函數里設置了一個prototype屬性,這一設計理念成為了js跟其他面向對象語言不同的地方,同時也埋下了巨大的坑!

為了解決因為委托機制帶來的各種各樣的缺點及語法問題,es6之后引入的class,class的實質還是基於原型鏈封裝的語法糖,但是卻大大簡化的前端開發的代碼,也解決了很多歷史遺留的問題,(這里並不想展開討論)。但是,es6之后,原型鏈真的不需要被了解了嗎?在知乎上有一篇被瀏覽了130多萬的話題 :《面試一個5年的前端,卻連原型鏈也搞不清楚,滿口都是Vue,React之類的實現,這樣的人該用嗎?曾經引起過熱議。接下來我們就來聊聊js的原型鏈吧!

關於 new 操作符

在聊原型鏈之前,我想先聊聊new,這是一個經常會在面試中被問到的基礎問題。怎么使用這里不詳細介紹,只是提一下js里new的設計原理:

  1. 創建一個新對象;

  2. 讓空對象的[[prototype]](IE9以下沒有該屬性,在js代碼里寫法為__proto__)成員指向了構造函數的prototype成員對象;

  3. 使用apply調用構造器函數,this綁定到空對象obj上;

  4. 返回新對象。

function NEW_OBJECT(Foo){
    var obj={};
    obj.__proto__=Foo.prototype;
    obj.constructor=Foo;
    Foo.apply(obj,arguments)
    return obj;
}

構造函數的主要問題是,每個方法都要再每個實例上重新創建一遍,不同實例上的同名函數是不相等的。例如:

function Person(name, age, job){
   this.name = name;
   this.age = age;
   this.job = job;
   this.sayName = function(){
     alert(this.name);
   };
}
var person1 = new Person("Nicholas"29"Software Engineer");
var person2 = new Person("Greg"27"Doctor");

alert(person1.sayName == person2.sayName); /*false*/

然而,創建兩個完成同樣任務的Function 實例的確沒有必要,通過把函數定義轉移到構造函數外部來解決這個問題。

function Person(name, age, job){
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = sayName;
}
function sayName(){
  alert(this.name);
}
var person1 = new Person("Nicholas"29"Software Engineer");
var person2 = new Person("Greg"27"Doctor");

新問題又來了:在全局作用域中定義的函數實際上只能被某個對象調用,這讓全局作用域有點名不副實。而更讓人無法接受的是:如果對象需要定義很多方法,那么就要定義很多個全局函數,於是我們這個自定義的引用類型就絲毫沒有封裝性可言了。這時候,該原型鏈登場了!

原型

1:[[prototype]]

JavaScript 中的對象有一個特殊的[[Prototype]] 內置屬性,其實就是對於其他對象的引用。幾乎所有的對象在創建時[[Prototype]] 屬性都會被賦予一個非空的值。所有普通的[[Prototype]] 鏈最終都會關聯到內置的Object.prototype。

當我們試圖訪問一個對象下的某個屬性的時候,會在JS引擎觸發一個GET的操作,首先會查找這個對象是否存在這個屬性,如果沒有找的話,則繼續在prototype關聯的對象上查找,以此類推。如果在后者上也沒有找到的話,繼續查找的prototype,這一系列的鏈接就被稱為原型鏈

2:prototype

只要創建了一個新函數,就會根據一組特定的規則為該函數創建一個prototype屬性

3:constructor

對象的.constructor 會默認關聯一個函數,這個函數可以通過對象的.prototype引用,.constructor 並不是一個不可變屬性。它是不可枚舉的,但是它的值是可寫的(可以被修改)。._ proto _ === .constructor.prototype

function Foo() /* .. */ }
Foo.prototype = { /* .. */ }; // 創建一個新原型對象,並改寫constructor
var a1 = new Foo();
a1.constructor === Foo; // false!
a1.constructor === Object// true!

(原型)繼承

四種寫法的思考

1:A.prototype = B.prototype

這種方法很容易理解,A要繼承B原型鏈屬性,直接改寫A的Prototype關聯到B的prototype,但是,如果在A上執行從B繼承過來的某一個屬性或方法,例如:A.prototype.myName =…會直接修改B.prototype本身。

2:A.prototype = new B()

這種方式會創建關聯到B原型上的新對象,但是由於使用構造函數,在B上如果修改狀態、主車道其他對象,會影響到A的后代。

3:A.prototype = Object.create(B.prototype) (ES5新增)

Object.create()是個很有意思的函數,用一段簡單的polyfill來實現它的功能:

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

Object.create(null) 會創建一個擁有空( 或者說null)[[Prototype]]鏈接的對象,這個對象因為沒有原型鏈無法進行委托

var anotherObject = {
  coolfunction() {
     console.log( "cool!" );
  }
};
var myObject = Object.create( anotherObject );

myObject.doCool = function() {
  this.cool(); // 內部委托!
};

myObject.doCool(); // "cool!"

4:Object.setPrototypeOf( A.prototype, B.prototype ); (ES6新增)

深度剖析 instanceof,徹底理解原型鏈

在segementfault上有這么一道面試題:

var str = new String("hello world");
console.log(str instanceof String);//true
console.log(String instanceof Function);//true
console.log(str instanceof Function);//false

先把這道題放一邊,我們都知道typeof可以判斷基本數據類型,如果是判斷某個值是什么類型的對象的時候就無能為力了,instanceof用來判斷某個 構造函數 的prototype是否在要檢測對象的原型鏈上。

function Fn(){};
var fn = new Fn();
console.log(fn instanceof Fn) //true

//判斷fn是否為Fn的實例,並且是否為其父元素的實例
function Aoo();
function Foo();
Foo.prototype = new Aoo();

let foo = new Foo();
console.log(foo instanceof Foo);  //true
console.log(foo instanceof Aoo);  //true

//instanceof 的復雜用法

console.log(Object instanceof Object)      //true
console.log(Function instanceof Function)  //true
console.log(Number instanceof Number)      //false
console.log(Function instaceof Function)   //true
console.log(Foo instanceof Foo)            //false

看到上面的代碼,你大概會有很多疑問吧。有人將ECMAScript-262 edition 3中對instanceof的定義用代碼翻譯如下:

function instance_of(L, R{//L 表示左表達式,R 表示右表達式
    var O = R.prototype;// 取 R 的顯示原型
    L = L.__proto__;// 取 L 的隱式原型
    while (true) { 
        if (L === null
            return false
        if (O === L)// 這里重點:當 O 嚴格等於 L 時,返回 true 
            return true
        L = L.__proto__; 
    } 
}

我們知道每個對象都有proto([[prototype]])屬性,在js代碼中用__proto__來表示,它是對象的隱式屬性,在實例化的時候,會指向prototype所指的對象;對象是沒有prototype屬性的,prototype則是屬於構造函數的屬性。通過proto屬性的串聯構建了一個對象的原型訪問鏈,起點為一個具體的對象,終點在Object.prototype。

Object instanceof Object :

// 區分左側表達式和右側表達式
ObjectL = Object, ObjectR = Object
O = ObjectR.prototype = Object.prototype;
L = ObjectL.__proto__ = Function.prototype (  Object作為一個構造函數,是一個函數對象,所以他的__proto__指向Function.prototype)
// 第一次判斷
O != L 
// 循環查找 L 是否還有 __proto__ 
L = Function.prototype.__proto__ = Object.prototype  (  Function.prototype是一個對象,同樣是一個方法,方法是函數,所以它必須有自己的構造函數也就是Object)
// 第二次判斷
O == L 
// 返回 true

Foo instanceof Foo :

FooL = Foo, FooR = Foo; 
// 下面根據規范逐步推演
O = FooR.prototype = Foo.prototype 
L = FooL.__proto__ = Function.prototype 
// 第一次判斷
O != L 
// 循環再次查找 L 是否還有 __proto__ 
L = Function.prototype.__proto__ = Object.prototype 
// 第二次判斷
O != L 
// 再次循環查找 L 是否還有 __proto__ 
L = Object.prototype.__proto__ = null 
// 第三次判斷
L == null 
// 返回 false

理解了這兩條判斷的原理,我們回到剛才的面試題:

console.log(str.__proto__ === String.prototype); //true
console.log(str instanceof String);//true

console.log(String.__proto__ === Function.prototype) //true
console.log(String instanceof Function);//true

console.log(str__proto__ === String.prototype)//true
console.log(str__proto__.__proto__. === Function.prototype) //true
console.log(str__proto__.__proto__.__proto__ === Object.prototype) //true
console.log(str__proto__.__proto__.__proto__.__proto__ === null//true
console.log(str instanceof Function);//false

總結以上,str的原型鏈是:

str ---String.prototype --->  Function.prototype ---Object.prototype

最后,提一個可以通用的來判斷原始數據類型和引用數據類型的方法吧:Object.prototype.toString.call()

ps:在js中,valueOf跟toString是兩個神奇的存在!!!

console.log(Object.prototype.toString.call(123)) //[object Number]
console.log(Object.prototype.toString.call('123')) //[object String]
console.log(Object.prototype.toString.call(undefined)) //[object Undefined]
console.log(Object.prototype.toString.call(true)) //[object Boolean]
console.log(Object.prototype.toString.call({})) //[object Object]
console.log(Object.prototype.toString.call([])) //[object Array]
console.log(Object.prototype.toString.call(function(){})) //[object Function]

最后提一下js中不倫不類的class

面向委托 VS 類:

我覺得可能畢竟面向對象的很多語言都有類,而js的繼承很多學習過其他語言的摸不着頭腦,就導致了js一直向模仿類的形式發展,es6就基於原型鏈的語法糖封裝了一個不倫不類的class,讓人以為js實際上也有類,真得是為了讓類似學習過java的朋友容易理解,狠起來連自己都騙!我很同意你不知道的javascript作者對於js中封裝類的看法:ES6 的class 想偽裝成一種很好的語法問題的解決方案,但是實際上卻讓問題更難解決而且讓JavaScript 更加難以理解。

這兩個的區別我並不想說太多,因為實際上我對類的理解也不多,只知道它的思想是定義好一個子類之后,相對於父類來說它就是一個獨立並且完全不同的類。子類會包含父類行為的原始副本,但是也可以重寫所有繼承的行為甚至定義新行為。子對父是真正的復制。

而在js中沒有真正意思的復制,實質上都是基於一個委托機制,復制的只是一個引用(類似C語言中指針的理解,js高程中習慣用指針思維來解釋,不過我更喜歡你不知道的javascript中的委托機制的說法。)

class的用法不再提,寫到這里,已經寫的很累了,盡管在一年前寫過類似的文章,但是重新整理起來還是不太輕松的一件事,而且我現在也覺得對於JS的類理解的不是那么透徹,以后再慢慢深入理解吧!


參考文獻:

1: JS高程設計 第六章
2: 你不知道的JavaScript(上卷)
3: JavaScript instanceof 運算符深入剖析
4: Javascript中一個關於instanceof的問題

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM