《JavaScript 物件導向精要》 讀書筆記

SHERlocked93發表於2019-03-22

高程物件導向這塊內容介紹的比較淺顯,個人覺得這本小書是高程的補充,看完之後覺得收穫匪淺,所以做了個筆記,以備後詢

1. 原始型別和引用型別

Js中兩種基本資料型別:原始型別(基本資料型別)和引用型別原始型別儲存為簡單資料值,引用型別則儲存為物件,其本質是指向記憶體位置的應用。 其它程式語言用棧儲存原始型別,用堆儲存引用型別,而js則不同:它使用一個變數物件追蹤變數的生存期。原始值被直接儲存在變數物件裡,而引用值則作為一個指標儲存在變數物件內,該指標指向實際物件在記憶體中的儲存位置。

1.1 原始型別(基本資料型別)

Js中一共有5種原始型別:booleannumberstringnullundefined,除了null型別,都可以用typeof來判斷 原始型別的變數直接儲存原始值(而不是一個指向物件的指標),當原始值被賦給一個變數,該值將被複制到變數中,每個變數有它自己的一份資料拷貝

var color1='red',color2=color1
console.log(color1)    // red
console.log(color2)    // red
color1='blue'
console.log(color2)    // red
複製程式碼

1.2 引用型別

物件(引用值)是引用型別的例項。物件是屬性的無序列表,屬性包含鍵和值,如果一個屬性的值是函式,它就被稱為方法; Js中的函式其實是引用值,除了函式可以執行以外,一個包含陣列的屬性和一個包含函式的屬性沒什麼區別。 Js中的建構函式用首字母大寫來跟非建構函式區分:var object = new Object() 因為引用型別不在變數中直接儲存物件,所以object變數實際上並不包含物件的例項,而是一個指向記憶體中實際物件所在位置的指標。

var object1 = new Object()
var object2 = object1
複製程式碼

一個變數賦值給另一個變數時,兩個變數各獲得一個指標的拷貝,並且指向同一個記憶體中的物件例項。 物件不使用時可以將引用解除:object = null,記憶體中的物件不再被引用時,垃圾收集器(GC)會把那塊記憶體挪作他用,在大型專案中尤為重要

1.3 原始封裝型別

原始封裝型別共3種:StringNumberBoolean,使用起來跟物件一樣方便,當讀取這三種型別時,原始封裝型別將被自動建立:

var name = "Nicholas"
var fisrtChar = name.charAt(0)
console.log(firstChar)                        // N
複製程式碼

背後發生的故事:

// what js engine does
var name = "Nicholas"
var temp = new String(name)            // 字串物件
var firstChar = temp.charAt(0)
temp = null
console.log(firstChar)                         // N
複製程式碼

Js引擎建立了一個字串的例項讓charAt(0)可以工作,字串物件的存在僅用於該語句並且在隨後被銷燬(一種被稱為自動打包的過程)。可以測試:

var name = "Nicholas"
name.last = "zakas"
console.log(name.last)                // undefined
複製程式碼

原始封裝型別的屬性會消失是因為被新增屬性的物件立刻就被銷燬了。 背後的故事:

var name = "Nicholas"
var temp = new String(name)
temp.last = "zakas"
temp = null                                            // temp物件銷燬

var temp = new String(name)
console.log(temp.last)                           // undefined
temp = null
複製程式碼

實際上是在一個立刻就被銷燬的臨時物件上而不是字串上新增了新的屬性,之後試圖再訪問該屬性,另一個不同的臨時物件被建立,而新屬性並不存在。雖然原始封裝型別會被自動建立,在這些值上進行的instanceof檢查對應型別的返回值卻是false

var name = 'Nicholas', count = 10, found = false
console.log(name instanceof String)                    // false
console.log(count instanceof Number)                // false
console.log(found instanceof Boolean)                // false
複製程式碼

這是因為臨時物件僅在值(屬性)被讀取時被建立,instanceof操作符並沒有真的讀取任何東西,也就沒有臨時物件的建立。 如果使用手動建立物件和原始封裝型別之間有一定區別,比如:

var found = new Boolean(false)
if (found) {
    console.log("Found")            // 執行了,因為物件在if條件判斷時總被認為是true,無論該物件是不是false,所以儘量避免手動建立原始封裝型別
}
複製程式碼

2. 函式

使函式不同於其它物件是函式存在一個[[Call]]的內部屬性。內部屬性無法通過程式碼訪問而是定義了程式碼執行時的行為。ECMAScript為Js的物件定義了多種內部屬性,這些內部屬性都用[[ ]]來標註。[[Call]]屬性表明該物件可以被執行,由於僅函式擁有該屬性,ECMAScript定義typeof操作符對任何具有[[Call]]屬性的物件返回function

2.1 函式宣告與函式表示式

函式有兩種字面形式,函式宣告函式表示式,兩者有個非常重要的區別,函式宣告會被提升至上下文的頂部(要麼是函式宣告時所在函式的範圍,要麼是全域性範圍),這意味著可以先使用再宣告函式。

2.2 函式就是值

函式可以像使用物件一樣使用,可以將它們賦給變數,在物件中新增它們,將它們當成引數傳遞給別的函式,或從別的函式中返回,基本上只要是可以使用其它引用值的地方,就可以使用函式。

2.3 引數

函式的引數實際上被儲存在一個arguments的陣列中,arguments可以自由增長來包含任意個數的值,它的length屬性可以告訴當前有多少個值。 arguments物件自動存在於函式中。也就是說函式的命名引數不過是為了方便,並不真的限制了函式可接受引數的個數。

注意: arguments物件不是一個陣列的例項,其擁有的方法與陣列不同,Array.isArray(arguments)返回false

函式期望的引數個數儲存在函式的length屬性中。

2.4 過載

Js中不存在簽名,因此也不存在過載,宣告的同名函式後一個會覆蓋前一個。 不過可以對arguments物件獲取的引數個數進行判斷來決定怎麼處理。

2.5 物件方法

可以像新增屬性那樣給物件新增方法,注意定義資料屬性和方法的語法完全相同。

var person = {
    name: "Nicholas",
    sayName: function () {
        console.log(person.name)
    }
}
複製程式碼

2.5.1 this物件

之前的例子的sayName()直接引用了person.name,在方法和物件之間建立了緊耦合,這種緊耦合使得一個方法很難被不同物件使用。 Js所有函式作用域內都有一個this物件代表該函式的物件。在全域性作用域內,this代表全域性物件window,當一個函式作為物件的方法被呼叫時,預設this的值等於那個物件。改寫:

var person = {
    name: "Nicholas",
    sayName: function () {
        console.log(this.name)  
    }
}
複製程式碼

所以應該在方法內引用this而不是直接引用物件。可以輕易改變變數名,或者將函式用在不同物件上,而不用大量改動程式碼。

function sayNameForAll() {
    console.log(this.name)
}
var person1={
    name: "Nicholas",
    sayName: sayNameForAll
}
var person2={
    name: "Greg" ,
    sayName: sayNameForAll
}
var name = "Micheal"
person1.sayName()                            // Nicholas
person2.sayName()                            // Greg
sayNameForAll()                                // Micheal
複製程式碼

this在函式被呼叫時才被設定,因此最後sayNameForAll函式執行時的this為全域性物件。

2.5.2 改變this

有3種方法可以改變this,函式是物件,而物件可以有方法,所以函式也有方法。

call()

第一個用於操作this的方法是call(),它以指定的this和引數來執行函式,第一個引數為函式執行時的this的值,後面的引數為需要被傳入函式的引數。

function sayNameForAll (label) {
    console.log(label + ':' + this.name)
}
var person1 = {name: "Nicholas"}
var person2 = {name: "Greg"}
var name = "Micheal"
sayNameForAll.call(this,"global")                        // global:Micheal
sayNameForAll.call(person1, "person1")             // person1:Nicholas
sayNameForAll.call(person2,"person2")              // person2:Greg
複製程式碼
apply()

第二個用於操作this的方法時apply(),其工作方式與call()完全一樣,但它只接受兩個引數:this的值和一個陣列或者類似陣列的物件,內含需要被傳入函式的引數(可以把arguments物件作為apply的第二個引數)。

function sayNameForAll (label) {
    console.log(label + ":" + this.name)
}
var person1 =  {name:"Nicholas"}
var person2 = {name:"Greg"}
var name = "Micheal"
sayNameForAll.apply(this,["global"])                        // global:Micheal
sayNameForAll.apply(person1, ["person1"])             // person1:Nicholas
sayNameForAll.apply(person2,["person2"])              // person2:Greg
複製程式碼

如果你已經有個陣列,那麼推介使用apply(),如果你有的是單獨的變數,則用call()

bind()

改變this的第三個函式方法為bind()bind()的第一個引數是要傳給新函式的this的值,其他引數代表需要被永久設定在新函式中的命名引數,可以在之後繼續設定任何非永久引數。

function sayNameForAll (label) {
    console.log(label + ":" + this.name)
}
var person1 =  {name:"Nicholas"}
var person2 = {name:"Greg"}

var sayNameForPerson1 = sayNameForAll.bind(person1)
sayNameForPerson1("person1")                                                // person1:Nicholas
var sayNameForPerson2 = sayNameForAll.bind(person2,"person2")
sayNameForPerson2()                                                                // person2:Greg
person2.sayName = sayNameForPerson1;
person2.sayName("person2")                                                    // person2:Nicholas
複製程式碼

sayNameForPerson1()沒有繫結永久引數,因此可以繼續傳入label引數輸出,sayNameForPerson2()不僅繫結了person2作為this,而且繫結了第一個引數為person2,因此可以使用sayNameForPerson2()而不用傳入額外引數,但是也不能更改了。person2.sayName最後由於this的值在sayNameForPerson1的函式表示式中已經繫結為person1了,所以雖然sayNameForPerson1現在是person2的方法,它依然輸出person1.name的值。

3. 理解物件

Js中的物件是動態的,可以在程式碼執行的任意時刻發生改變。

3.1 定義屬性

當一個屬性第一次被新增給物件時,Js在物件上隱式呼叫一個名為[[Put]]的內部方法,[[Put]]方法會在物件上建立一個新節點儲存屬性,就像第一次在雜湊表上新增一個鍵一樣。這個操作不僅指定了初試的值,也定義了屬性的一些特徵。 呼叫[[Put]]的結果是在物件上建立了一個自有屬性,該屬性被直接儲存在例項內,對該屬性的所有操作都必須通過該物件進行。 當一個已有的屬性被賦予一個新值時,呼叫的是一個名為[[Set]]的方法,該方法將屬性的當前值替換為新值。

3.2 屬性探測

由於屬性可以在任何時候新增,因此有時候有必要檢查物件是否已有該屬性:

if(person1.age){            // 不可取
    // 執行
}
複製程式碼

問題在於Js的型別強制會影響輸出結果,如果if判斷的值為null、undefined、0、false、NaN或者空字串時則判斷為假。由於一個物件屬性可以包含這些假值,上例程式碼可能導致錯誤的判斷,更可靠的判斷是用in操作符。 in操作符是在給定物件上查詢一個給定名稱的屬性,如果找到則返回true,另外in操作符在判斷的時候不會評估屬性的值:

var person1={
    name: "Nicholas",
    age: "111",
    sayName:function(){
        consloe.log(this.name)
    }
}
console.log('name' in person1)            // true
console.log('age' in person1)              // true
console.log('title' in person1)              // false
console.log('sayName' in person1)            // true    方法是值為函式的屬性,因此同樣可以用in判斷
複製程式碼

但是in操作符會檢查自有屬性和原型屬性,因此在只想要自有屬性的時候使用hasOwnProperty()判斷一下,該方法在給定的屬性存在並且為自有屬性時返回true。

3.3 刪除屬性

正如屬性可以在任何時候被新增,也可以在任何時候被刪除。但是設定一個屬性值為null並不能將其從物件中刪除,只是呼叫[[Set]]將null替換了該屬性原來的值。徹底的刪除屬性值需要delete操作符。 delete操作符針對單個物件呼叫[[Delete]]的內部方法,可以認為該操作在雜湊表中移除了一個鍵值對,當delete操作符成功時,它返回true。

注意: 某些屬性無法被delete

var person1= {name: 'Nicholas'}
console.log('name' in person1)                // true
delete person.name
console.log('name' in person1)                // false
console.log(person1.name)                          // undefined
複製程式碼

3.4 屬性列舉

所有你新增的屬性預設為可列舉的,可以用for-in迴圈遍歷,可列舉屬性的內部特徵[[Enumerable]]都被設定為true。for-in迴圈會列舉一個物件中所有的可列舉屬性並將屬性名賦給一個物件:

var property
for (property in object){
    console.log('name:' + property)
    console.log('value' + object[property])
}
複製程式碼

如果只需要獲取一個物件的屬性列表,ES5引入了Object.keys()方法,它可以獲取可列舉屬性的名字(key)的陣列。

注意:Object.keys()只返回自有屬性不返回原型屬性。

var properties = Object.keys(object)
var i, len=properties.length
for (i=0; i<len; i++){
    console.log('name:' + properties[i])
    console.log('value' + object[properties[i]])
}
複製程式碼

並不是每個屬性都是可列舉的,可以使用propertyIsEnumerable()方法檢查一個屬性是否為可列舉,每個物件都有該方法。

var person1= {name: 'Nicholas'}
var properties = Object.keys(person1)
console.log('name' in person1)                                                // true
console.log(person1.propertyIsEnumerable('name'))            // true
console.log('length' in properties)                                            // true
console.log(properties.propertiesIsEnumerable('length'))            // false
複製程式碼

這裡name為可列舉,因為它是person1的自有屬性,而propertieslength為不可列舉的,因為它是Array.prototype的內建屬性,你會發現很多原生屬性預設都是不可列舉的。

3.5 屬性型別

屬性有兩種型別資料屬性訪問器屬性資料屬性包含一個值,例如之前的name屬性,[[Put]]方法預設行為是建立一個資料屬性。 訪問器屬性不包含值而是定義了一個當屬性被讀取時呼叫的函式getter和一個當屬性被寫入時呼叫的函式setter

let person1 = {
    _name: "Nicholas" ,                        // 前置下劃線是約定俗成的,表示該屬性為私有的,實際上它是公開的
    get name() {
        console.log("reading me")
        return this._name
    },
    set name(val) {
        console.log(`setting name to ${val}`)
        this._name = val
    }
}
console.log(person1.name)                // reading me Nicholas
person1.name='greg'
console.log(person1.name)                // setting name to Greg
複製程式碼

用於定義namegettersetter的語法看上去像函式但是沒有function關鍵字,注意getset之後的name需要跟被訪問的屬性名保持一致。 當你希望賦值操作會觸發一些行為或者讀取的值需要通過計算所需的返回值得到時,訪問器屬性將會很有用。

注意: 不一定要同時定義gettersetter,可以選擇其中之一,如果只定義getter,那麼屬性變為只讀,在非嚴格下寫入將失敗,嚴格下寫入報錯,如果只定義setter,那麼屬性為只寫,兩種模式下讀取都失敗

3.6 屬性特徵

ES5之前無法訪問屬性的任何特徵,也沒有辦法指定一個屬性是否為可列舉,因此ES5引入多種方法與屬性特徵互動,同時也引入新的特徵來支援額外的功能,現在已經可以建立出和Js內建屬性一樣的自定義屬性。下面介紹資料屬性和訪問器屬性的特徵。

3.6.1 通用特徵

有兩個屬性時資料屬性和訪問器屬性共有的: [[Enumerable]]決定你是否可以遍歷該屬性; [[Configurable]]決定該屬性是否可配置; 你可以用delete刪除一個可配置的屬性,或者隨時改變它,也可以把可配置的屬性從資料屬性變為訪問器屬性,反之亦可,所有自有屬性都是可列舉和可配置的。

如果你想改變屬性特徵,可以使用Object.defineProperty()方法,它接受三個引數:擁有函式的物件、屬性名、包含需要設定的特徵的屬性描述物件。屬性描述物件具有和內部特徵同名的屬性但名字中不包含中括號,所以可以使用enumerable屬性來設定[[Enumerable]]特徵,用configurable屬性來設定[[Configurable]]特徵。假如你想讓一個物件屬性變成不可列舉且不可配置:

var person1 = { name: 'Nicholas' }
var properties = Object.keys(person1)

Object.defineProperty(person1, 'name', { enumerable: false })
console.log('name' in person1)                          // true
console.log(person1.propertyIsEnumerable('name'))      // false
console.log(properties.length)                            // 0
Object.defineProperty(person1, 'name', { configurable: false })
delete person1.name                                            // 屬性設定為不可配置之後不能被delete,刪除失敗
console.log('name' in person1)                        // true
console.log(person1.name)                            // Nicholas
Object.defineProperty(person1, 'name', { configurable: true })    // error!    設定為不可配置之後就不能再設定屬性特徵了,包括[[Configurable]]
複製程式碼

3.6.2 資料屬性特徵

資料屬性額外擁有兩個訪問器屬性不具備的特徵: [[Value]]包含屬性的值,當你在物件上建立屬性時該特徵被自動賦值,所有屬性的值都儲存在[[Value]]中,哪怕該值是一個函式; [[Writable]]是一個布林值,指示該屬性是否可以寫入,所有屬性預設都是可寫的,除非另外指定。 通過這兩個額外屬性,可以使用Object.defineProperty()完整定義一個資料屬性,即使該屬性還不存在。

var person1 = { name: 'Nicholas' }                // 等同於
Object.defineProperty(person, 'name',  {
    value: "Nicholas",
    enumerable: true,
    configurable: true,
    writable: true
}
複製程式碼

Object.defineProperty()被呼叫時,它首先檢查屬性是否存在,如果不存在將根據屬性描述物件指定的特徵建立。當使用Object.defineProperty()定義新屬性時一定記得為所有的特徵指定一個值,否則布林型的特徵會被預設設定為false。

var person1 = {}
Object.defineProperty(person1, 'name', { value: 'Nicholas' })    // 由於沒有顯式指定特徵,因此屬性為不可列舉、不可配置、不可寫的
console.log('name' in person1)                          // true
console.log(person1.propertyIsEnumerable('name'))      // false
delete person1.name
console.log('name' in person1)                    // true
person1.name = 'Greg'
console.log(person1.name)                          // Nicholas
複製程式碼

在嚴格模式下檢視改變不可寫屬性會丟擲錯誤,而在非嚴格模式下會失敗

3.6.3 訪問器屬性

訪問器屬性擁有兩個資料屬性不具備的特徵,訪問器屬性不需要儲存值,因此也就沒有[[Value]][[Writable]],取而代之的是[[Get]][[Set]]屬性,內含gettersetter函式,同字面量形式一樣,只需要定義其中一個特徵就可以建立一個訪問器屬性。

如果試圖建立一個同時具有資料屬性和訪問器屬性的屬性,會報錯

之前get set 例子可以被改寫為:

let person1 = { _name: "Nicholas" }
Object.defineProperty(person1, 'name', {
    get: function() {
      console.log("reading me")
      return this._name
    },
    set: function(val) {
      console.log(`setting name to ${val}`)
      this._name = val
    },
    enumerable: true,
    configurable: true
  }
)
console.log(person1.name)               // reading me Nicholas
person1.name = 'greg'
console.log(person1.name)                // setting name to Greg
複製程式碼

注意Object.defineProperty()中的get和set關鍵字,它們是包含函式的資料屬性,這裡不能使用字面量形式。

3.6.4 定義多重屬性

如果你使用Object.defineProperties()而不是Object.defineProperty()可以為一個物件同時定義多個屬性,這個方法接受兩個引數:需要改變的物件、一個包含所有屬性資訊的物件。後者可以背看成一個雜湊表,鍵是屬性名,值是為該屬性定義特徵的屬性描述物件。

var person1 = {}
Object.defineProperties(person1, {
  _name: {
    value: 'Nicholas',
    enumerable: true,
    configurable: true,
    writable: true
  },
  name: {
    get: function() {
      console.log('reading me')
      return this._name
    },
    set: function(val) {
      console.log(`setting name to ${val}`)
      this._name = val
    },
    enumerable: true,
    configurable: true
  }
})
複製程式碼

3.6.5 獲取屬性特徵

如果需要獲取屬性的特徵,Js中可以使用Object.getOwnPropertyDescriptor(),這個方法只可以用於自有屬性,它接受兩個引數:物件、屬性名。如果屬性存在,它會返回一個屬性描述物件,內含四個屬性:configurable、enumerable、另外兩個根據屬性型別決定。即使你從沒有為屬性顯式指定特徵,你依然會得到包含全部這些特徵值的屬性描述物件。

3.7 禁止修改物件

物件和屬性一樣具有指導行為的內部特徵,其中,[[Extensible]]是一個布林值,它指明該物件本身是否可以被修改,你建立的所有物件預設都是可擴充套件的,新的屬性可以隨時被新增,設定[[Extensible]]為false則可以禁止新屬性的新增。 下面有三種方法可以用來鎖定物件屬性

3.7.1 禁止擴充套件

第一種方法是Object.preventExtensions()建立一個不可擴充套件的物件。該方法接受一個引數:你希望擴充套件的物件。一旦在一個物件上用這個方法,就永遠不能再給它新增新的屬性了。

let person1 = { _name: "Nicholas" }
console.log(Object.isExtensible(person1))            // true
Object.preventExtensions(person1)
console.log(Object.isExtensible(person1))            // false
person1.sayName = function(){
    console.log(this.name)
}
console.log('sayName' in person1)                // false
複製程式碼

在嚴格模式下試圖給一個不可擴充套件物件新增屬性會丟擲錯誤,而在非嚴格模式下會失敗。應該對不可擴充套件物件使用嚴格模式,這樣當一個不可擴充套件物件被錯誤使用時你就會知道

3.7.2 物件封印

一個被封印的物件是不可擴充套件的且其所有屬性都不可配置,這意味著不僅不能給物件新增屬性,而且也不能刪除屬性或改變型別(從資料屬性改變成訪問屬性或者反之),如果一個物件被封印,那麼只能讀寫它的屬性。 可以用Object.seal()方法來封印一個物件,該方法被呼叫時[[Extensible]]特徵被設定為false,其所有屬性的[[Configurable]]特徵被置為false,可以使用Object.isSealed()來判斷一個物件是否被封印。 這段程式碼封印了person1,因此不能再person1上新增或者刪除屬性。所有的被封印物件都是不可擴充套件的物件,此時對person1使用Object.isExtensible()方法將會返回false,且檢視新增sayName()會失敗。 而且雖然person.name被成功改變成一個新值,但是刪除它會失敗。

確保對被封印的物件使用嚴格模式,這樣當有人誤用該物件時,會報錯

3.7.3 物件凍結

被凍結的物件不能新增或刪除屬性,不能修改屬性型別,也不能寫入任何資料屬性。簡言而之,被凍結物件是一個資料屬性都為只讀的被封印物件。 Object.freeze() 凍結物件。 Object.isFrozen() 判斷物件是否被凍結。

被凍結物件僅僅只是物件在某個時間點上的快照,用途有限且很少被使用

4. 建構函式和原型物件

4.1 建構函式

建構函式就是用new建立物件時呼叫的函式,使用建構函式的好處在於所有用同一個建構函式建立的物件都具有同樣的屬性和方法。 建構函式也是函式,定義的方式和普通函式一樣,唯一的區別是建構函式名應該首字母大寫,以此區分。

function Person(){}
var person1 = new Person                        // 如果沒有要傳遞給建構函式的引數,括號可以省略
console.log(person1 instanceof Person)        // true
console.log(person1.constructor === Person)        // true
複製程式碼

即使Person建構函式沒有顯式返回任何東西,person1也會被認為是一個新的Person型別的物件,new操作符會自動建立給定型別的物件並返回它們。每個物件在建立時都會自動擁有一個建構函式屬性,其中包含了一個指向其建構函式的引用。那些通過字面量形式或者Object建構函式建立出來的泛用物件,其建構函式屬性constructer指向Object;那些通過自定義建構函式建立出來的物件,其建構函式屬性指向建立它的建構函式。

雖然物件例項及其建構函式之間存在這樣的關係,但是還是建議使用instanceof來檢查物件型別,這是因為建構函式屬性可以被覆蓋,並不一定完全準確。 在建構函式中只需簡單的給this新增任何想要的屬性即可:

function Person(name){
    this.name =  name
    this.sayName() = function(){
        console.log(this.name)
    }
}
複製程式碼

在呼叫建構函式時,new會自動建立this物件,且其型別就是建構函式的型別,建構函式本身不需要返回一個物件,new操作符會幫你返回。

function Person2(name){
    this.name=name
    this.sayName=function(){
        console.log(this.name)
    }
}
var person2=new Person2('sam') 
console.log(person2.name)                    // sam
person2.sayName()                                // sam
複製程式碼

每個物件都有自己的name屬性值,所以sayName可以根據不同物件返回不同的值。

也可以在建構函式中顯式呼叫return,如果返回的是一個物件,那麼它會替代新建立的物件例項返回,如果返回的是一個原始型別,那麼它將被忽略,新建立的物件例項將被返回。

建構函式允許使用一致的方法初始化一個型別的例項,在使用物件前設定好所有的屬性,可以在建構函式中使用Object.defineProperty()的方法來幫助初始化。

function Person(name) {
    Object.defineProperty(this, 'name', {
        get: function() {
            return name
        },
        set: function(newName) {
            name = newName
        },
        enumerable: true,
        configurable: true
    })

    this.sayName = function() {
        console.log(this.name)
    }
}

var person1 =new Person('Nicholas')                // 始終確保使用了new操作符,否則就是冒著改變全域性物件的風險
console.log(person1 instanceof Person)            // true   
console.log(typeof person1)                               // object
console.log(name)                                              // undefined
複製程式碼

當Person不是被new呼叫時候,建構函式中的this指向全域性物件,由於Person建構函式依靠new提供返回值,person1變數為undefined。沒有new,Person只不過是一個沒有返回語句的函式,對this.name的賦值實際上建立了一個全域性物件name。

嚴格模式下,不通過new呼叫Person建構函式會出現錯誤,這是因為嚴格模式並沒有為全域性物件設定this,this保持為undefined,而試圖給undefined新增屬性時都會出錯

建構函式允許給物件配置同樣的屬性,當建構函式並沒有消除程式碼冗餘,每個物件都有自己的sayName()方法,這意味著100個物件例項就有100個函式做相同的事情,只是使用的資料不同。如果所有的物件例項共享同一個方法會更有效率,該方法可以使用this.name來訪問對應的資料,這就需要用到原型物件

4.2 原型物件

原型物件可以看做物件的基類,幾乎所有函式(除了一下內建函式)都有一個名為prototype的屬性,該屬性是一個原型物件用來建立新的物件例項。 所有建立的物件例項共享該原型物件,且這些物件例項可以訪問原型物件的屬性。例如,hasOwnProperty()方法被定義在泛用物件Object的原型物件中,但卻可以被任何物件當做自己的屬性訪問。

var book = {title: "the principles of object-oriented js"}
console.log('title' in book)
console.log(book.hasOwnProperty('title'))                        // true
console.log('hasOwnProperty' in book)                            // true
console.log(book.hasOwnProperty('hasOwnProperty'))             // false
console.log(Object.prototype.hasOwnProperty('hasOwnProperty'))            // true
複製程式碼

即使book中沒有hasOwnProperty()方法的定義,但仍然可以通過book.hasOwnProperty()訪問該方法,這是因為該方法存在於Object.prototype中。 可以使用這樣一個方法來判斷一個屬性是否為原型屬性:

function hasPrototypeProperty(object, name){
    return name in object && !object.hasOwnProperty(name)
}
複製程式碼

4.2.1 [[Prototype]]屬性

一個物件例項通過內部屬性[[Prototype]]追蹤其原型物件,該 屬性時一個指向該例項使用的原型物件的指標。當你使用new建立一個新的物件時,建構函式的原型物件會被賦給該物件的[[Prototype]]屬性 (JS proto 探究.md )。你可以呼叫Object.getPropertyOf()方法讀取[[prototype]]屬性的值。

Object.prototype.proto === null

var object={}
Object.getPrototypeOf(object) === Object.prototype                // true
Object.prototype.isPrototypeOf(object)                    // true
複製程式碼

任何一個泛用物件(字面量形式或者new Object()),其[[Prototype]]物件始終指向Object.prototype。也可以用isPrototypeOf()方法檢查某個物件是否是另一個物件的原型物件,該方法被包含在所有物件中。

**Note:**大部分Js引擎在所有物件上都支援一個__proto__的屬性,該屬性使你可以直接讀寫[[Prototype]]屬性。包括Firefox、Safari、Chrome、Node.js

在讀取一個物件的屬性時,Js引擎會首先在物件的自有屬性中查詢屬性名字,如果找到則返回,如果沒有則Js會搜尋[[Prototype]]中的物件,如果找到則返回,找不到則返回undefined

var object = {}
console.log(object.toString())                    // [object Object]
object.toString = function() {return "[object Custom]"}
console.log(object.toString())                    // [object Custom]
delete object.toString
console.log(object.toString())                    // [object Object]
delete object.toString
console.log(object.toString())                    // [object Object]
複製程式碼

上例可以看出,delete運算子只對只有屬性起作用,無法刪除一個物件的原型屬性。並且也不可以給一個物件的原型屬性賦值,對.toString的賦值只是在物件上建立了一個新的自有屬性,而不是改變原型屬性。

4.2.2 在建構函式中使用原型物件

原型物件的共享機制使得它們成為一次性為所有物件定義所有方法的理想手段,因為一個方法對所有的物件例項做相同的事,沒理由每個例項都要有一份自己的方法。將方法放在原型物件中並使用this方法當前例項是更有效的做法。

function Person(name) {this.name = name}
Person.prototype.sayName = function() {console.log(this.name)};
var person1 = new Person("Nicholas")
console.log(person1.name)                        // Nicholas
person1.sayName()                                // Nicholas
複製程式碼

也可以在原型物件上儲存其他型別的資料,但是在儲存引用值時要注意,因為這些引用值會被多個例項共享,可能大家不希望一個例項能夠改變另一個例項的值。

function Person(name) {this.name = name}
Person.prototype.favorites = []
var person1 = new Person("Nicholas")
var person2 = new Person("Greg")
person1.favorites.push("pizza")
person2.favorites.push("quinoa")

console.log(person1.favorites)                // ["pizza", "quinoa"]
console.log(person2.favorites)                // ["pizza", "quinoa"]
複製程式碼

favorites屬性被定義到原型物件上,意味著person1.favoritesperson2.favorites指向同一個陣列,你對任意Person物件的favorites插入的值都將成為原型物件上陣列的元素。也可以使用字面量的形式替換原型物件:

function Person(name) {this.name=name}
Person.prototype= {
    sayName: function() {console.log(this.name)},
    toString: function(){return `[Person ${this.name} ]`}
}
複製程式碼

雖然用這種字面量的形式定義原型非常簡潔,但是有個副作用需要注意。

var person1 = new Person('Nicholas')
console.log(person1 instanceof Person)                // true
console.log(person1.constructor === Person)                // false
console.log(person1.constructor === Object)                // true
複製程式碼

使用字面量形式改寫原型物件改寫了建構函式的屬性,因此現在指向Object而不是Person,這是因為原型物件具有個constructor屬性,這是其他物件例項所沒有的。當一個函式被建立時,其prototype屬性也被建立,且該原型物件的constructor屬性指向該函式自己,當使用字面量形式改寫原型物件Person.prototype時,其constructor屬性將被複寫為泛用物件Object。為了避免這一點,需要在改寫原型物件時手動重置其constructor屬性:

function Person(name) {this.name = name}
Person.prototype = {
    constructor: Person,             // 為了不忘記賦值,最好在第一個屬性就把constructor重置為自己
    sayName() {console.log(this.name)},
    toString() {return `[Person ${this.name} ]`}
}

var person1 = new Person('Nicholas')
console.log(person1 instanceof Person)                    // true
console.log(person1.constructor === Person)                // true
console.log(person1.constructor === Object)                // false
複製程式碼

建構函式、原型物件、物件例項之間:物件例項和建構函式之間沒有直接聯絡。不過物件例項和原型物件之間以及原型物件和建構函式之間都有直接聯絡。

這樣的連線關係也意味著,如果打斷物件例項和原型物件之間的聯絡,那麼也將打斷物件例項及其建構函式之間的關係。

4.2.3 改變原型物件

給定型別的所有物件例項共享一個原型物件,所以可以一次性擴充所有物件例項。[[Prototype]]屬性只是包含了一個指向原型物件的指標,任何對原型物件的改變都將你可反映到所有引用它的物件例項上。這意味著給原型物件新增的新成員都可以立刻被所有已經存在的物件例項使用。

function Person(name) {this.name = name}
Person.prototype = {
    constructor: Person,
    sayName() {console.log(this.name)},
    toString() {return `[Person ${this.name} ]`}
}
var person1 = new Person('Nicholas')
var person2 = new Person('Greg')
console.log('sayHi' in person1)                // false
console.log('sayHi' in person2)                // false
Person.prototype.sayHi = () => console.log("Hi")
person1.sayHi()                // Hi
person2.sayHi()                // Hi
複製程式碼

當對一個物件使用Object.seal()Object.freeze()封印和凍結物件的時候是在操作物件的自有屬性,無法新增封印物件的自有屬性和更改凍結物件的自有屬性,但是仍然可以通過在原型物件上新增屬性來擴充套件物件例項:

function Person(name) {this.name = name}
var person1 = new Person("Nicholas")
Object.freeze(person1)
Person.prototype.sayHi = function() {console.log("Hi")};
person1.sayHi()            // Hi
複製程式碼

其實,[[Prototype]]是例項物件的自有屬性,屬性本身person1.[[Prototype]]被凍結,但是指向的值Person.prototype並沒有凍結。

4.2.4 內建物件的原型物件

所有內建物件都有建構函式,因此也都有原型物件可以去改變,例如要在陣列上新增一個新的方法只需要改變Array.prototype即可

Array.prototype.sum = function() {
    return this.reduce((privious, current) => privious + current)
}
var numbers = [1, 2, 3, 4, 5, 6]
var result = numbers.sum()
console.log(result)                    // 21
複製程式碼

sum()函式內部,在呼叫時this指向陣列的物件例項numbers,因此this也可以呼叫該陣列的其他方法,比如reduce()。 改變原始封裝型別的原型物件,就可以給這些原始值新增更多功能,比如:

String.prototype.capitalize = function() {
    return this.charAt(0).toUpperCase() + this.substring(1)
}
var message = 'hello world!'
console.log(message.capitalize())            // Hello world!
複製程式碼

5. 繼承

5.1 原型物件鏈和Object.prototype

Js內建的繼承方法被稱為原型物件鏈,又稱為原型物件繼承。原型物件的屬性可以由物件例項訪問。例項物件整合了原型物件的屬性,因為原型物件也是一個物件,它也有自己的原型物件並繼承其屬性。這就是原型繼承鏈:物件繼承其原型物件,而原型物件繼承它的原型物件,以此類推。 所有的物件,包括自定義的物件都繼承自Object,除非另有指定。更確切的說,所有物件都繼承自Object.prototype,任何以字面量形式定義的物件,其[[Prototype]]的值都被設為Object.prototype,這意味著它繼承Object.prototype的屬性。

var book = {title: 'a book'}
console.log(Object.getPrototypeOf(book) === Object.prototype)            // true
複製程式碼

5.1.1 繼承自Object.prototype的方法

前幾張用到的幾個方法都是定義在Object.prototype上的,因此可以被其他物件繼承:

Methods Usage
hasOwnProperty() 檢查是否存在一個給定名字的自有屬性
propertyIsEnumerable() 檢查一個自有屬性是否為可列舉
isPrototypeOf() 檢查一個物件是否是另一個物件的原型物件
valueOf() 返回一個物件的值表達
toString() 返回一個物件的字串表達

這幾種方法由繼承出現在所有的物件中,當你需要物件在Js中以一致的方式工作時,最後兩個尤為重要。

  1. valueOf() 每當一個操作符被用於一個物件時就會呼叫valueOf()方法,其預設返回物件例項本身。原始封裝型別重寫了valueOf()以使得它對String返回一個字串,對Boolean返回一個布林,對Number返回一個數字;類似的,對Date物件的valueOf()返回一個epoch時間,單位是毫秒(正如Data.prototype.getTime())。

    var now = new Date                // now.valueOf()  === 1505108676169
    var earlier = new Date(2010,1,1)            // earlier.valueOf() === 1264953600000
    console.log(now>earlier)                // true
    console.log(now-earlier)                 // 240155076169
    複製程式碼

    now是一個代表當前時間的Date,而earlier是過去的時間,當使用操作符>時,兩個物件上都呼叫了valueOf()方法,你甚至可以用兩個Date相減來獲得它們在epoch時間上的差值。如果你的物件也要這樣使用操作符,你可以定義自己的valueOf()方法,定義的時候你並沒有改變操作符的行為,僅僅應了操作符預設行為所使用的值。

  2. toString() 一旦valueOf()返回的是一個引用而不是原始值的時候,就會回退呼叫toString()方法。另外,當Js期望一個字串時也會對原始值隱式呼叫toString()。例如當加號操作符的一邊是一個字串時,另一邊就會被自動轉換成字串,如果另一邊是一個原始值,會自動轉換成一個字串表達(true => "true"),如果另一邊是一個引用值,則會呼叫valueOf(),如果其返回一個引用值,則呼叫toString()

    var book = {title: 'a book'}
    console.log("book = " + book)                // "book = [object Object]"
    複製程式碼

    因為book是一個物件,因此呼叫它的toString()方法,該方法繼承自Object.prototype,大部分Js引擎返回預設值[object Object],如果對這個值不滿意可以複寫,為此類字串提供包含跟多資訊。

    var book = {title: 'a book',
    toString(){return `[Book = ${this.title} ]`}}
    console.log("book = " + book)                 // book = [Book = a book ]
    複製程式碼

5.1.2 修改Object.prototype

所有的物件都預設繼承自Object.prototype,所以改變它會影響到所有的物件,這是非常危險的。 如果給Obejct.prototype新增一個方法,它是可列舉的,可以粗現在for-in迴圈中,一個空物件依然會輸出一個之前新增的屬性。儘量不要修改Object.prototype。

5.2 物件繼承

物件字面量形式會隱式指定Object.prototype為其[[Prototype]],也可以用Object.create()方式顯示指定。Object.create()方法接受兩個引數:需要被設定為新物件[[Prototype]]的物件、屬性描述物件,格式如在Object.defineProperties()中使用的一樣(第三章)。

var book = {title: 'a book'}
// ↑ 等價於 ↓
var book = Object.create(Object.prototype, {
    title: {
        configurable: true,
        enumerable: true,
        value: 'a book',
        writable: true
    }
})
複製程式碼

第一種寫法中字面量形式定義的物件自動繼承Object.prototype且其屬性預設設定為可配置、可寫、可列舉。第二種寫法顯示使用Object.create()做了相同的操作,兩個book物件的行為完全一致。

var person = {
    name: "Jack",
    sayName: function(){
        console.log(this.name);
    }
}

var student = Object.create(person, {
    name:{value: "Ljc"},
    grade: {
        value: "fourth year of university",
        enumerable: true,
        configurable: true,
        writable: true
    }
});

person.sayName(); // "Jack"
student.sayName(); // "Ljc"

console.log(person.hasOwnProperty("sayName")); // true
console.log(person.isPrototypeOf(student)); // true
console.log(student.hasOwnProperty("sayName")); // false
console.log("sayName" in student); // true

console.log(student.__proto__===person)                                      // true
console.log(student.__proto__.__proto__===Object.prototype)      // true
複製程式碼

《JavaScript 物件導向精要》 讀書筆記

物件person2繼承自person1,也就整合了person1的name和sayName(),然而又通過重寫name屬性定義了一個自有屬性,隱藏並替代了原型物件中的同名屬性。所以person1.sayName()輸出Nicholas而person2.sayName()輸出Greg。 在訪問一個物件的時候,Js引擎會執行一個搜尋過程,如果在物件例項上發現該屬性,該屬性值就會被使用,如果沒有發現則搜尋[[Prototype]],如果仍然沒有發現,則繼續搜尋該原型物件的[[Prototype]],知道繼承鏈末端,末端通常是一個Object.prototype,其[[prototype]]為null。這就是原型鏈。 當然也可以通過Object.create()建立[[Prototype]]為null的物件:var obj=Object.create(null)。該物件obj是一個沒有原型鏈的物件,這意味著toString()valueOf等存在於Object原型上的方法都不存在於該物件上。

5.3 建構函式繼承

Js中的物件繼承也是建構函式繼承的基礎,第四章提到:幾乎所有的函式都有prototype屬性(通過Function.prototype.bind方法構造出來的函式是個例外),它可以被替換和修改。該prototype屬性被自動設定為一個繼承自Object.prototype的泛用物件,該物件有個自有屬性constructor

// 建構函式
function YourConstructor() {}
// Js引擎在背後做的:
YourConstructor.prototype = Object.create(Object.prototype, {
    constructor: {
        configurable: true,
        enumerable: true,
        value: YourConstructor,
        writable: true
    }
})
console.log(YourConstructor.prototype.__proto__===Object.prototype)            // true
複製程式碼

你不需要做額外工作,Js引擎幫你把建構函式的prototype屬性設定為一個繼承自Object.prototype的物件,這意味著YourConstructor建立出來的任何物件都繼承自Object.prototype,YouConstructor是Object的子類。 由於prototype可寫,可以通過改寫它來改變原型鏈:

function Rectangle(length, width) {
    this.length = length
    this.width = width
}
Rectangle.prototype.getArea = function() {return this.length * this.width};
Rectangle.prototype.toString = function() {return `[ Rectangle ${this.length}x${this.width} ]`};

function Square(size) {
    this.length = size
    this.width = size
}
Square.prototype = new Rectangle()
Square.prototype.constructor = Square
Square.prototype.toString = function() {return `[ Square ${this.length}x${this.width} ]`}

var rect = new Rectangle(5, 10)
var squa = new Square(6)
console.log(rect instanceof Rectangle)        // true
console.log(rect instanceof Square)        // false
console.log(rect instanceof Object)        // true
console.log(squa instanceof Rectangle)        // true
console.log(squa instanceof Square)        // true
console.log(squa instanceof Object)        // true
複製程式碼

MDN:instanceof 運算子可以用來判斷某個建構函式的 prototype 屬性是否存在另外一個要檢測物件的原型鏈上。

Square建構函式的prototype屬性被改寫為Rectagle的一個例項,此時不需要給Rectangle的呼叫提供引數,因為它們不需要被使用,而且如果提供了,那麼所有的Square物件例項都會共享這樣的維度。如果用這種方式改寫原型鏈,需要確保建構函式不會再引數缺失時丟擲錯誤(很多建構函式包含的初始化邏輯)且建構函式不會改變任何全域性狀態。

// inherits from Rectangle
function Square(size){
    this.length = size;
    this.width = size;
}

Square.prototype = new Rectangle(); // 儘管是 Square.prototype 是指向了 Rectangle 的物件例項,即Square的例項物件也能訪問該例項的屬性(如果你提前宣告瞭該物件,且給該物件新增屬性)。
// Square.prototype = Rectangle.prototype; // 這種實現沒有上面這種好,因為Square.prototype 指向了 Rectangle.prototype,導致修改Square.prototype時,實際就是修改Rectangle.prototype。
console.log(Square.prototype.constructor); // 輸出 Rectangle 建構函式

Square.prototype.constructor = Square; // 重置回 Square 建構函式
console.log(Square.prototype.constructor); // 輸出 Square 建構函式

Square.prototype.toString = function(){
    return "[Square " + this.length + "x" + this.width + "]";
}

var rect = new Rectangle(5, 10);
var square = new Square(6);

console.log(rect.getArea()); // 50
console.log(square.getArea()); // 36

console.log(rect.toString()); // "[Rectangle 5 * 10]", 但如果是Square.prototype = Rectangle.prototype,則這裡會"[Square 5 * 10]"
console.log(square.toString()); // "[Square 6 * 6]"

console.log(square instanceof Square); // true
console.log(square instanceof Rectangle); // true
console.log(square instanceof Object); // true
複製程式碼

《JavaScript 物件導向精要》 讀書筆記

Square.prototype 並不真的需要被改成為一個 Rectangle物件。事實上,是 Square.prototype 需要指向 Rectangle.prototype 使得繼承得以實現。這意味著可以用 Object.create() 簡化例子。

// inherits from Rectangle
function Square(size){
    this.length = size;
    this.width = size;
}

Square.prototype= Object.create(Rectangle.prototype, {
    constructor: {
        configurable: true,
        enumerable: true,
        value: Square,
        writable: true
    }
})
複製程式碼

在對原型物件新增屬性前要確保你已經改寫了原型物件,否則在改寫時會丟失之前新增的方法(因為繼承是將被繼承物件賦值給需要繼承的原型物件,相當於重寫了需要繼承的原型物件)。

5.4 建構函式竊取

由於JavaScript中的繼承是通過原型物件鏈來實現的,因此不需要呼叫物件的父類的建構函式。如果確實需要在子類建構函式中呼叫父類建構函式,那就可以在子類的建構函式中利用 call、apply方法呼叫父類的建構函式。

function Rectangle(length, width) {
    this.length = length
    this.width = width
}
Rectangle.prototype.getArea = function() {return this.length * this.width};
Rectangle.prototype.toString = function() {return `[ Rectangle ${this.length}x${this.width} ]`};

function Square(size) {Rectangle.call(this, size, size)}
Square.prototype = Object.create(Rectangle.prototype, {
    constructor: {
        value: Square,
        enumerable: true,
        configurable: true,
        writable: true
    }
})
Square.prototype.toString = function() {return `[ Square ${this.length}x${this.width} ]`}

var rect = new Rectangle(5, 10)
var squa = new Square(6)
console.log(rect.getArea())
console.log(rect.toString())
console.log(squa.getArea())
console.log(squa.toString())
複製程式碼

一般來說,需要修改 prototype 來繼承方法並用建構函式竊取來設定屬性,由於這種做法模仿了那些基於類的語言的類繼承,所以這通常被稱為偽類繼承

5.5 訪問父類方法

其實也是通過指定 callapply 的子物件呼叫父類方法。

6. 物件模式

可以使用繼承或者混入等其他技術令物件間行為共享,也可以利用Js高階技巧阻止物件結構被改變。

6.1 私有成員和特權成員

6.1.1 模組模式

模組模式是一種用於建立擁有私有資料的單件物件的模式。 基本做法是使用立即呼叫函式表示式(IIFE)來返回一個物件。原理是利用閉包。

var yourObj = (function(){
    // private data variables   
    return {
        // public methods and properties
    }
}());
複製程式碼

模組模式還有一個變種叫暴露模組模式,它將所有的變數和方法都放在 IIFE 的頭部,然後將它們設定到需要被返回的物件上。

//  一般寫法
var yourObj = (function(){
    var age = 25;    
    return {
        name: "Ljc",      
        getAge: function(){
            return age 
        }
    }
}());

// 暴露模組模式,保證所有變數和函式宣告都在同一個地方
var yourObj = (function(){
    var age = 25;                            // 私有變數,外部無法訪問
    function getAge(){
        return age
    };
    return {
        name: "Ljc",                          // 公共變數外部可以訪問
        getAge: getAge                    // 外部可以訪問的物件
    }
}());
複製程式碼

6.1.2 建構函式的私有成員

模組模式在定義單個物件的私有屬性十分有效,但對於那些同樣需要私有屬性的自定義型別呢?你可以在建構函式中使用類似的模式來建立每個例項的私有資料。

function Person(name){
    // define a variable only accessible inside of the Person constructor
    var age = 22;   
    this.name = name;
    this.getAge = function(){return age;};
    this.growOlder = function(){age++;}
}

var person = new Person("Ljc");
console.log(person.age);         // undefined
person.age = 100;
console.log(person.getAge());         // 22
person.growOlder();
console.log(person.getAge());         // 23
複製程式碼

建構函式在被new的時候建立了一個本地作用於並返回this物件。這裡有個問題:如果你需要物件例項擁有私有資料,就不能將相應方法放在 prototype上。 如果你需要所有例項共享私有資料(就好像它被定義在原型物件裡那樣),則可結合模組模式和建構函式,如下:

var Person = (function(){
    var age = 22;
    function InnerPerson(name){this.name = name;}
    InnerPerson.prototype.getAge = function(){return age;}
    InnerPerson.prototype.growOlder = function(){age++;};
    return InnerPerson;
}());

var person1 = new Person("Nicholash");
var person2 = new Person("Greg");
console.log(person1.name); // "Nicholash"
console.log(person1.getAge()); // 22
console.log(person2.name); // "Greg"
console.log(person2.getAge()); // 22

person1.growOlder();
console.log(person1.getAge()); // 23
console.log(person2.getAge()); // 23
複製程式碼

6.2 混入

這是一種偽繼承。一個物件在不改變原型物件鏈的情況下得到了另外一個物件的屬性被稱為“混入”。因此,和繼承不同,混入讓你在建立物件後無法檢查屬性來源。

function mixin(receiver, supplier){
    for(var property in supplier){
        if(supplier.hasOwnProperty(property)){
            receiver[property] = supplier[property];
        }
    }
}
複製程式碼

這是淺拷貝,如果屬性的值是一個引用,那麼兩者將指向同一個物件。 要注意一件事,使用這種方式,supplier的訪問器屬性會被複製為receiver的資料屬性。

function mixin(reciver, supplier) {
    if (Object.getOwnPropertyDescriptor) {                    // 檢查是否支援es5
        Object.keys(supplier).forEach(property => {
            var descriptor = Object.getOwnPropertyDescriptor(supplier, property)
            Object.defineProperty(reciver, property, descriptor)
        })
    } else {
        for (var property in supplier) {                        // 否則使用淺複製
            if (supplier.hasOwnProperty(property)) {
                reciver[property] = supplier[property]
            }
        }
    }
}
複製程式碼

6.3 作用域安全的建構函式

建構函式也是函式,所以不用 new 也能呼叫它們來改變 this 的值。在非嚴格模式下, this 被強制指向全域性物件。而在嚴格模式下,建構函式會丟擲一個錯誤(因為嚴格模式下沒有為全域性物件設定 this,this 保持為 undefined)。 而很多內建建構函式,例如 Array、RegExp 不需要 new 也能正常工作,這是因為它們被設計為作用域安全的建構函式。 當用 new 呼叫一個函式時,this 指向的新建立的物件已經屬於該建構函式所代表的自定義型別。因此,可在函式內用 instanceof 檢查自己是否被 new 呼叫。

function Person(name){
    if(this instanceof Person){
        // called with "new"
    }else{
        // called without "new"
    }
}
複製程式碼

具體案例:

function Person(name){
    if(this instanceof Person){
        this.name = name;
    }else{
        return new Person(name);
    }
}
複製程式碼

PS:歡迎大家關注我的公眾號【前端下午茶】,一起加油吧~

《JavaScript 物件導向精要》 讀書筆記

相關文章