lodash原始碼分析(create.js)

breakinferno發表於2017-12-21
本系列使用lodash 4.17.4

前言

沒有引用任何檔案

正文

/**
 * Creates an object that inherits from the `prototype` object. If a
 * `properties` object is given, its own enumerable string keyed properties
 * are assigned to the created object.
 *
 * @since 2.3.0
 * @category Object
 * @param {Object} prototype The object to inherit from.
 * @param {Object} [properties] The properties to assign to the object.
 * @returns {Object} Returns the new object.
 * @example
 *
 * function Shape() {
 *   this.x = 0
 *   this.y = 0
 * }
 *
 * function Circle() {
 *   Shape.call(this)
 * }
 *
 * Circle.prototype = create(Shape.prototype, {
 *   'constructor': Circle
 * })
 *
 * const circle = new Circle
 * circle instanceof Circle
 * // => true
 *
 * circle instanceof Shape
 * // => true
 */
function create(prototype, properties) {
  prototype = prototype === null ? null : Object(prototype)
  const result = Object.create(prototype)
  return properties == null ? result : Object.assign(result, properties)
}

export default create
複製程式碼

這個函式的作用是建立某個原型物件的新物件,可以傳入可列舉的配置屬性對新生成的物件進行屬性的配置修改,主要使用到的還是Object.create()函式。雖然這個函式程式碼簡短,但是設計到原型、及其基於原型的繼承和物件屬性拷貝,涉及的知識點還是比較多的。下面我就簡單的分析一下包含的知識點然後再回來看這段程式碼及其例項吧。

1.原型


我們都知道javascript是基於原型的一門語言。原型是javascript中比較重要也是比較難的一點。初學的時候很容易被捲入雞生蛋,蛋生雞的死衚衕裡。不信,我們來試試幾個題:

Function.prototype === Function.__proto__   //true 
Function.prototype === Object.__proto__     //true
Function.prototype.__proto__ === Object.prototype //true
Object.prototype === Object.__proto__       //false
Function instanceof Function                //true
Object instanceof Object                    //true
Object instanceof Function                  //true
Function instanceof Object                  //true
Object.__proto__ instanceof Object          //true
Object.prototype instanceof Object          //false
Function.prototype instanceof Object        //true
Function.__proto__ instanceof Object        //true
先有Function還是先有Object建構函式?          //沒有先後順序,如果有那也是作者編寫程式碼時的先後

/*所有物件由建構函式生成,而建構函式也是物件,那這個建構函式物件是怎麼生成的呢?如果把物件比作雞,建構函式比作蛋,那就是雞生蛋蛋生雞的悖論*/
/*歸根到底就是
Function建構函式本身也算是Function型別的例項嗎?
Function建構函式的prototype屬性和__proto__屬性都指向同一個原型,是否可以說Function物件是由Function建構函式建立的一個例項?*/
/*這一悖論的根源來自Object和Function互為對方的例項*/
複製程式碼

如果對上述問題你都知道,並且對其有較深的理解,那麼你可能已經理解了什麼是原型。當然如果你對這幾道題還有疑惑,那麼你就需要更加深入的理解這一知識點。

好了,話說回來,我來簡單的複習一下原型的知識點和應當注意的地方,並且解決掉雞生蛋蛋生雞的問題。

根據js紅寶書講的,每一個建構函式A建立的時候都會建立另外一個物件,這個物件為A.prototype,(我們知道函式也是一個物件,所以以後就叫函式為函式物件,還要注意不是所有函式都有prototype屬性,比如Object.prototype.toString.bind(Array)),此時該函式物件存在一個屬性叫prototype,其指向為這個自動建立的物件(原型物件)。並且使用該建構函式建立例項時該例項a會存在__proto__屬性(其實是[[Prototype]]屬性,只不過Firefox和Chrome提供了"__proto__"這個非標準的訪問器)來指向該原型物件A.prototype。所以我們可以知道當建立一個建構函式和例項化其建構函式生成的例項以及自動建立的物件(原型物件)關係如下:

lodash原始碼分析(create.js)

這裡我們可以提出幾個疑問了?

  1. 原型物件是怎麼建立的或者說是通過什麼方式建立的,new 還是 物件字面量?有沒有建構函式?如果有,建構函式是什麼?
  2. 建構函式物件又是怎麼建立的?有沒有建構函式?如果有,建構函式是什麼?
  3. 所有物件都是建構函式的例項嗎?或者所有物件都是Object的例項嗎?
  4. 雞生蛋蛋生雞問題

要解決這幾個問題我們需要了解幾個重要概念(哲學三問~~~~)

1.什麼是物件,什麼是例項物件

An object is a collection of properties and has a single prototype object. The prototype may be the null value.

所有的具有屬性的資料集合就是物件。而例項物件是由建構函式建立的一個具象化物品,所有的例項物件都是物件,而不是所有物件都是例項物件。比如最常見的Object.prototype就是內建物件,並不是任何建構函式的例項。當然我們遇到的大多數都是例項物件。

2.什麼是函式

ECMAScript規範定義的函式:

物件型別的成員,標準內建構造器 Function的一個例項,並且可做為子程式被呼叫。
注: 函式除了擁有命名的屬性,還包含可執行程式碼、狀態,用來確定被呼叫時的行為。函式的程式碼不限於 ECMAScript。

函式就是建構函式Function的例項。那什麼是建構函式呢?ECMAScript規範如此定義:

建立和初始化物件的函式物件
注:構造器的“prototype”屬性值是一個原型物件,它用來實現繼承和共享屬性。

建構函式物件作為一個函式物件均例項化自建構函式Function(建構函式物件是例項化物件的一部分),當然建構函式Function也是函式物件,也例項化自Function(這裡你需要明白例項化是將原型物件的屬性拷貝到例項化物件中,而Function的原型Function.prototype是個內建物件)。所以如果不考慮繼承的話,建構函式物件都是基於Function.prototype這個內建物件的。

注:對於Function是否是Function建構函式例項存在一定的爭議

3.什麼是原型

ECMAScript標準如下:

為其他物件提供共享屬性的物件。
當構造器建立一個物件,為了解決物件的屬性引用,該物件會隱式引用構造器的“prototype”屬性。通過程式表示式 constructor.prototype 可以引用到構造器的“prototype”屬性,並且新增到物件原型裡的屬性,會通過繼承與所有共享此原型的物件共享。另外,可使用 Object.create 內建函式,通過明確指定原型來建立一個新物件。

原型就是為物件提供共享屬性的物件,不然每個物件的屬性都是私有的,導致空間浪費之類的問題。其建立是在建立建構函式的時候隱式建立的。其屬性constructor指向建構函式,而建構函式的prototype屬性指向原型物件。

4.我們說一個物件是某個建構函式例項的根據是啥

我們一般通過instanceof 判斷物件是否是建構函式的例項,而instanceof 實際是呼叫hasInstance,而hasInstance的迴圈呼叫prototype直到最頂上那個物件,如果這個與第二個引數的prototype一致,那就是真,否則假。也就是遍歷例項物件的原型鏈並判斷建構函式的原型是否在該原型鏈上。是Object.getPrototypeOf(Function) === Function.prototype 或者簡單來說Object.__proto__ === Function.prototype的結果,只是一種運算關係,滿足這種關係就判定是該建構函式的例項。

好了回到正題,我們一一解決上述問題:

1.原型物件通過呼叫Object.create()來建立,是Object的例項。可以通過A.prototype.__proto__  === Object.prototype來說明

2.建構函式物件通過建構函式的建構函式Function來建立,建構函式物件是Function的例項。由於建構函式Function也是物件,所以Function既是雞也是蛋。具體的請看4.

3.什麼是物件那裡已經回答了,不是所有物件都是Object的例項

4.要解決雞生蛋蛋生雞問題,不能從javascript的角度上思考,必須跳出來從編譯角度上看這一問題。javascript的實現不是javascript,而是更為底層的語言,比如c,c++之類的。我們可以很輕易的知道Function這個函式物件是內建物件(不曉得是通過啥語言建立了這個物件),只不過其屬性prototype和__proto__都指向另外一個內建物件Function.prototype,所以從我們javascript語法的角度就會得到悖論,但實際上是該物件先存在,只不過表現為這種關係罷了。(感覺只要說不過去扯上內建物件都可以說過去啊23333)

扯不下去了。下面貼一個對原型解釋很好的圖:

lodash原始碼分析(create.js)

給個別人的解釋:

JavaScript引擎是個工廠。
最初,工廠做了一個最原始的產品原型。
這個原型叫Object.prototype,本質上就是一組無序key-value儲存({})
之後,工廠在Object.prototype的基礎上,研發出了可以儲存一段“指令”並“生產產品”的原型產品,叫函式。
起名為Function.prototype,本質上就是[Function: Empty](空函式)
為了規模化生產,工廠在函式的基礎上,生產出了兩個構造器:
生產函式的構造器叫Function,生產kv儲存的構造器叫Object。
你在工廠定製了一個產品,工廠根據Object.prototype給你做了一個Foo.prototype。
然後工廠發現你定製的產品很不錯。就在Function.prototype的基礎上做了一個Foo的構造器,叫Foo。
工廠在每個產品上打了個標籤__proto__,以標明這個產品是從哪個原型生產的。
為原型打了個標籤constructor,標明哪個構造器可以依照這個原型生產產品。
為構造器打了標籤prototype,標明這個構造器可以從哪個原型生產產品。
所以,我覺得先有Function還是Object,就看工廠先造誰了。其實先做哪個都無所謂。因為在你定製之前,他們都做好了。

再給個比較好的連結:

javascript 世界萬物誕生記

2.原型鏈和繼承

原型鏈就是如果一個例項物件a的原型物件A.prototype為另外一個例項物件b的話,a就可以共享a.__proto__,a.__proto__.__proto__(b.__proto__)上的屬性,這個原型之間鏈狀的東西我們就叫他原型鏈。其主要作用是用於繼承。

繼承是為了解決new的例項物件不能共享屬性和方法的缺點。不說啥了,上六種繼承方式:

只給連結,不想寫了。。。。。。有空的時候再修改和放圖吧。。。

繼承的六種方式

原始碼分析

1.Object()方法描述:

  • obj if obj is an object
  • {} if obj is undefined or null

這玩意兒和這個語句差不多:obj = obj || {},但是區別在於如果傳入的obj是原始值,比如4,這個方法會返回包裝過的Number物件而不是4這個原始值。

2.Object.create(prop):根據傳入的物件建立一個將該物件作為原型的物件,執行空的建構函式。

Object.create =  function (o) {
    var F = function () {};
    F.prototype = o;
    return new F();
};
複製程式碼

Object.create是內部定義一個物件,並且讓F.prototype物件 賦值為引進的物件/函式 o,並return出一個新的物件。

new做法是新建一個例項物件o,並且讓o的__proto__指向了原型物件。並且使用call 進行強轉作用環境。從而實現了例項的建立。

偷偷截張圖:

lodash原始碼分析(create.js)

3.Object.assign():主要用於物件屬性合併和新增。沒什麼好說的。注意的是其方式是淺拷貝,而不是深拷貝。也就是說,如果源物件某個屬性的值是物件,那麼目標物件拷貝得到的是這個物件的引用。如果要實現深拷貝需要遞迴的進行屬性拷貝。而且這個方法可以被...代替使用


使用方式

  function Shape() {
    this.x = 0
    this.y = 0
  }
 
  function Circle() {
    Shape.call(this)
  }
 
  Circle.prototype = create(Shape.prototype, {
    'constructor': Circle
  })
 
  const circle = new Circle
  circle instanceof Circle
  // => true

  circle instanceof Shape
  // => true複製程式碼

Object.create(proto[, propertiesObject])沒啥區別。用於實現類式繼承

使用場景

用於寄生組合式繼承避免多次呼叫建構函式。其他的。。。。沒時間了,,

結語

兩天趕完了,由於時間跨度原因,估計有些地方牛頭不對馬嘴。mayiga。。。



相關文章