《JavaScript面向對象精要》讀書筆記

JavaScript(ES5)的面向對象精要

標籤: JavaScript 面向對象 讀書筆記前端


2016年1月16日-17日兩天看完了《JavaScript面向對象精要》(參加異步社區的活動送的),這本書雖然不夠100頁,但都是精華,不愧是《JavaScript高級程序設計》做者 Nicholas C.Zakas 的最新力做。git

下面是個人讀書筆記(ES5):github

1.原始類型和引用類型

1.1 什麼是類型

原始類型 保存爲簡單數據值。
引用類型 保存爲對象,其本質是指向內存位置的引用。正則表達式

爲了讓開發者可以把原始類型和引用類型按相同的方式處理,JavaScript花費了很大的努力來保證語言的一致性。chrome

其餘編程語言用棧存原始類型,用對存儲引用類型。而JavaScript則徹底不一樣:它使用一個變量對象追蹤變量的生存期。原始值被直接保存在變量對象內,而引用值則做爲一個指針保存在變量對象內,該指針指向實際對象在內存中的存儲位置。編程

1.2 原始類型

原始類型表明照原樣保存的一些簡單數據。
JavaScript共有 5 種原始類型:api

  • boolean 布爾,值爲 true or false數組

  • number 數字,值爲任何整型或浮點數值瀏覽器

  • string 字符串,值爲由單引號或雙引號括住的單個字符或連續字符安全

  • null 空類型,僅有一個值:null

  • undefined 未定義,只有一個值:undefined(undefined會被賦給一個尚未初始化的變量)

JavaScript和許多其餘語言同樣,原始類型的變量直接保存原始值(而不是一個指向對象的指針)。

var color1 = "red";
var color2 = color1;

console.log(color1); // "red"
console.log(color2); // "red"

color1 = "blue";

console.log(color1); // "blue"
console.log(color2); // "red"

鑑別原始類型

鑑別原始類型的最佳方式是使用 typeof 操做符。

console.log(typeof "Nicholas"); // "string"
console.log(typeof 10);         // "number"
console.log(typeof true);       // "boolean"
console.log(typeof undefined);  // "undefined"

至於空類型(null)則有些棘手。

console.log(typeof null); // "object"

對於 typeof null,結果是"object"。(其實這已被設計和維護JavaScript的委員會TC39認定是一個錯誤。在邏輯上,你能夠認爲 null 是一個空的對象指針,因此結果爲"object",但這仍是很使人困惑。)

判斷一個值是否爲空類型(null)的最佳方式是直接和 null 比較:

console.log(value === null); // true or false

注意:以上這段代碼使用了三等號(全等===),由於三等號(全等)不會將變量強制轉換爲另外一種類型。

console.log("5" == 5); // true
console.log("5" === 5); // false

console.log(undefined == null); // true
console.log(undefined === null); // false

原始方法

雖然字符串、數字和布爾值是原始類型,可是它們也擁有方法(null和undefined沒有方法)。

var name = "Nicholas";
var lowercaseName = name.toLowerCase(); // 轉爲小寫

var count = 10;
var fixedCount = count.toFixed(2); // 轉爲10.00

var flag = true;
var stringFlag = flag.toString(); // 轉爲"true"

console.log("YIBU".charAt(0)); // 輸出"Y"

儘管原始類型擁有方法,但它們不是對象。JavaScript使它們看上去像對象同樣,以此來提升語言上的一致性體驗。

1.3 引用類型

引用類型是指JavaScript中的對象,同時也是你在該語言中能找到最接近類的東西。
引用值是引用類型的實例,也是對象的同義詞(後面將用對象指代引用值)。對象是屬性的無序列表。屬性包含鍵(始終是字符串)和值。若是一個屬性的值是函數,它就被稱爲方法。除了函數能夠運行之外,一個包含數組的屬性和一個包含函數的屬性沒有什麼區別。

建立對象

有時候,把JavaScript對象想象成哈希表能夠幫助你更好地理解對象結構。

Object

JavaScript 有好幾種方法能夠建立對象,或者說實例化對象。第一種是使用 new 操做符和構造函數。
構造函數就是經過 new 操做符來建立對象的函數——任何函數均可以是構造函數。根據命名規範,JavaScript中的構造函數用首字母大寫來跟非構造函數進行區分。

var object = new Object();

由於引用類型再也不變量中直接保存對象,因此本例中的 object 變量實際上並不包含對象的實例,而是一個指向內存中實際對象所在位置的指針(或者說引用)。這是對象和原始值之間的一個基本差異,原始值是直接保存在變量中。

當你將一個對象賦值給變量時,實際是賦值給這個變量一個指針。這意味着,將一個變量賦值給另一個變量時,兩個變量各得到了一份指針的拷貝,指向內存中的同一個對象。

var obj1 = new Object();
var obj2 = obj1;

copy_obj

對象引用解除

JavaScript語言有垃圾收集的功能,所以當你使用引用類型時無需擔憂內存分配。但最好在不使用對象時將其引用解除,讓垃圾收集器對那塊內存進行釋放。解除引用的最佳手段是將對象變量設置爲 null

var obj1 = new Object();
// dosomething
obj1 = null; // dereference

添加刪除屬性

在JavaScript中,你能夠隨時添加和刪除其屬性。

var obj1 = new Object();
var obj2 = obj1;

obj1.myCustomProperty = "Awsome!";
console.log(obj2.myCustomProperty); // "Awsome!" 由於obj1和obj2指向同一個對象。

1.4 內建類型實例化

內建類型以下:

  • Array 數組類型,以數字爲索引的一組值的有序列表

  • Date 日期和時間類型

  • Error 運行期錯誤類型

  • Function 函數類型

  • Object 通用對象類型

  • RegExp 正則表達式類型

可以使用 new 來實例化每個內建引用類型:

var items = new Array();
var new = new Date();
var error = new Error("Something bad happened.");
var func = new Function("console.log('HI');");
var object = new Object();
var re = new RegExp();

字面形式

內建引用類型有字面形式。字面形式容許你在不須要使用 new 操做符和構造函數顯示建立對象的狀況下生成引用值。屬性的能夠是標識符或字符串(若含有空格或其餘特殊字符)

var book = {
    name: "Book_name",
    year: 2016
}

上面代碼與下面這段代碼等價:

var book = new Object();
book.name = "Book_name";
book.year = 2016;

雖然使用字面形式並無調用 new Object(),可是JavaScript引擎背後作的工做和 new Object()同樣,除了沒有調用構造函數。其餘引用類型的字面形式也是如此。

1.5 訪問屬性

可經過 .中括號 訪問對象的屬性。
中括號[]在須要動態決定訪問哪一個屬性時,特別有用。由於你能夠用變量而不是字符串字面形式來指定訪問的屬性。

1.6 鑑別引用類型

函數是最容易鑑別的引用類型,由於對函數使用 typeof 操做符時,返回"function"。

function reflect(value){
    return value;
}
console.log(typeof reflect); // "function"

對其餘引用類型的鑑別則較爲棘手,由於對於全部非函數的引用類型,typeof 返回 object。爲了更方便地鑑別引用類型,可使用 JavaScript 的 instanceof 操做符。

var items = [];
var obj = {};
function reflect(value){
    return value;
}

console.log(items instanceof Array); // true;
console.log(obj instanceof Object); // true;
console.log(reflect instanceof Function); // true;

instanceof 操做符可鑑別繼承類型。這意味着全部對象都是 Oject 的實例,由於全部引用類型都繼承自 Object

雖然 instanceof 能夠鑑別對象類型(如數組),可是有一個列外。JavaScript 的值能夠在同一個網頁的不用框架之間傳來傳去。因爲每一個網頁擁有它本身的全局上下文——Object、Array以及其餘內建類型的版本。因此當你把一個對象(如數組)從一個框架傳到另一個框架時,instanceof就沒法識別它。

1.8 原始封裝類型

原始封裝類型有 3 種:String、Number 和 Boolean。
當讀取字符串、數字或布爾值時,原始封裝類型將被自動建立。

var name = "Nicholas";
var firstChar = name.charAt(0); // "N"

這在背後發生的事情以下:

var name = "Nichola";
var temp = new String(name);
var firstChar = temp.charAt(0);
temp = null;

因爲第二行把字符串當成對象使用,JavaScript引擎建立了一個字符串的實體讓 charAt(0) 能夠工做。字符串對象的存在僅用於該語句並在隨後銷燬(一種被稱爲自動打包的過程)。爲了測試這一點,試着給字符串添加一個屬性看看它是否是對象。

var name = "Nicholas";
name.last = "Zakas";

console.log(name.last); // undefined;

下面是在JavaScript引擎中實際發生的事情:

var name = "Nicholas";
var temp = new String(name);
temp.last = "Zakas";
temp = null; // temporary object destroyed

var temp = new String(name);
console.log(temp.last);
temp = null;

新屬性 last 其實是在一個馬上就被銷燬的臨時對象上而不是字符串上添加。以後當你試圖訪問該屬性時,另外一個不一樣的臨時對象被建立,而新屬性並不存在。

雖然原始封裝類型會被自動建立,在這些值上進行 instanceof 檢查對應類型的返回值倒是 false
這是由於臨時對象僅在值被讀取時建立instanceof 操做符並無真的讀取任何東西,也就沒有臨時對象的建立。

固然你也能夠手動建立原始封裝類型。

var str = new String("me");
str.age = 18;

console.log(typeof str); // object
console.log(str.age); // 18

如你所見,手動建立原始封裝類型實際會建立出一個 object。這意味着 typeof 沒法鑑別出你實際保存的數據的類型。

另外,手動建立原始封裝類型和使用原始值是有必定區別的。因此儘可能避免使用。

var found = new Boolean(false);
if(found){
    console.log("Found"); // 執行到了,儘管對象的值爲 false
}

這是由於一個對象(如 {} )在條件判斷語句中總被認爲是 true;

MDN:Any object whose value is not undefined or null, including a Boolean oject whose value is false, evaluates to true when passed to a conditional statement.

1.9 總結

第一章的東西都是咱們一些比較熟悉的知識。可是也有一些須要注意的地方:

  • 正確區分原始類型和引用類型

  • 對於 5 種原始類型均可以用typeof來鑑別,而空類型必須直接跟 null 進行全等比較。

  • 函數也是對象,可用 typeof 鑑別。其它引用類型,可用 instanceof 和一個構造函數來鑑別。(固然能夠用 Object.prototype.toString.call() 鑑別,它會返回[object Array]之類的)。

  • 爲了讓原始類型看上去更像引用類型,JavaScript提供了 3 種封裝類型。JavaScript會在背後建立這些對象使得你可以像使用普通對象那樣使用原始值。但這些臨時對象在使用它們的語句結束時就馬上被銷燬。雖然可手動建立,但不建議。

2. 函數

函數也是對象,使對象不一樣於其它對象的決定性特色是函數存在一個被稱爲 [[Call]] 的內部屬性。
內部屬性沒法經過代碼訪問而是定義了代碼執行時的行爲。ECMAScript爲JavaScript的對象定義了多種內部屬性,這些內部屬性都用雙重中括號來標註

[[Call]]屬性是函數獨有的,代表該對象能夠被執行。因爲僅函數擁有該屬性,ECMAScript 定義typeof操做符對任何具備[[Call]]屬性的對象返回"function"。過去因某些瀏覽器曾在正則表達式中包含 [[Call]] 屬性,致使正則表達式被錯誤鑑別爲函數。

2.1 聲明仍是表達式

二者的一個重要區別是:函數聲明會被提高至上下文(要麼是該函數被聲明時所在的函數範圍,要麼是全局範圍)的頂部。

2.2 函數就是值

能夠像使用對象同樣使用函數(由於函數原本就是對象,Function構造函數更加容易說明)。

2.3 參數

函數參數保存在類數組對象 argumentArray.isArray(arguments) 返回 false)中。能夠接收任意數量的參數。
函數的 length 屬性代表其指望的參數個數。

2.4 重載

大多數面嚮對象語言支持函數重載,它能讓一個函數具備多個簽名。函數簽名由函數的名字、參數的個數及其類型組成。
而JavaScript能夠接收任意數量的參數且參數類型徹底沒有限制。這說明JavaScript函數根本就沒有簽名,所以也不存在重載。

function sayMessage(message){
    console.log(message);
}
function sayMessage(){
    console.log("Default Message");
}

sayMessage("Hello!"); // 輸出"Default Message";

在Javscript裏,當你試圖定義多個同名的函數時,只有最後的定義有效,以前的函數聲明被徹底刪除(函數也是對象,變量只是存指針)。

var sayMessage = new Function("message", "console.log(message)");
var sayMessage = new Function("console.log(\"Default Message\");");

sayMessage("Hello!");

固然,你能夠根據傳入參數的數量來模仿重載。

2.5 對象方法

對象的值是函數,則該屬性被稱爲方法。

2.5.1 this對象

JavaScript 全部的函數做用域內都有一個 this 對象表明調用該函數的對象。在全局做用域中,this 表明全局對象(瀏覽器裏的window)。當一個函數做爲對象的方法調用時,默認 this 的值等於該對象。
this在函數調用時才被設置。

function sayNameForAll(){
    console.log(this.name);
}

var person1 = {
    name: "Nicholas",
    sayName: sayNameForAll
}

var name = "Jack";

person1.sayName(); // 輸出 "Nicholas"
sayNameforAll(); // 輸出 "Jack"

2.5.2 改變this

3 種函數方法運行你改變 this 值。

  1. fun.call(thisArg[, arg1[, arg2[, ...]]]);

  2. fun.apply(thisArg, [argsArray]);

  3. fun.bind(thisArg[, arg1[, arg2[, ...]]])

使用 callapply 方法,就不須要將函數加入每一個對象——你顯示地指定了 this 的值而不是讓JavaScript引擎自動指定。

callapply 的不一樣地方是,call 須要把全部參數一個個列出來,而 apply 的參數須要一個數組或者相似數組的對象(如 arguments 對象)。

bind 是ECMAScript 5 新增的,它會建立一個新函數返回。其參數與 call 相似,並且其全部參數表明須要被永久設置在新函數中的命名參數(綁定了的參數(沒綁定的參數依然能夠傳入),就算調用時再傳入其它參數,也不會影響這些綁定的參數)。

function sayNameForAll(label){
    console.log(label + ":" + this.name);
}
var person = {
    name: "Nicholas"
}

var sayNameForPerson = sayNameForAll.bind(person);
sayNameForPerson("Person"); // 輸出"Person:Nicholas"

var sayName = sayNameForAll.bind(person, "Jc");

sayName("change"); // 輸出"Jc:Nicholas" 由於綁定的形參,會忽略調用時再傳入參數

2.6 總結

  • 函數也是對象,因此它能夠被訪問、複製和覆蓋。

  • 函數與其餘對象最大的區別在於它們有一個特殊的內部屬性 [[Call]],包含了該函數的執行指令。

  • 函數聲明會被提高至上下文的頂部。

  • 函數是對象,因此存在一個 Function 構造函數。但這會使你的代碼難以理解和調試,除非函數的真實形式要直到運行時才能肯定的時候纔會利用它。

理解對象

JavaScript中的對象是動態的,可在代碼執行的任意時刻發生改變。基於類的語言會根據類的定義鎖定對象。

3.1 定義屬性

當一個屬性第一次被添加到對象時,JavaScript會在對象上調用一個名爲 [[Put]] 的內部方法。[[Put]] 方法會在對象上建立一個新節點來保存屬性。
當一個已有的屬性被賦予一個新值時,調用的是一個名爲 [[Set]] 的方法。

3.2 屬性探測

檢查對象是否已有一個屬性。JavaScript開發新手錯誤地使用如下模式檢測屬性是否存在。

if(person.age){
    // do something with ag
}

上面的問題在於JavaScript的類型強制會影響該模式的輸出結果。
當if判斷中的值以下時,會判斷爲

  • 對象

  • 非空字符串

  • 非零

  • true

當if判斷中的值以下時,會判斷爲

  • null

  • undefined

  • 0

  • false

  • NaN

  • 空字符串

所以判斷屬性是否存在的方法是使用 in 操做符。
in 操做符會檢查自有屬性和原型屬性
全部的對象都擁有的 hasOwnProperty() 方法(實際上是 Object.prototype 原型對象的),該方法在給定的屬性存在且爲自有屬性時返回 true

var person = {
    name: "Nicholas"
}

console.log("name" in person); // true
console.log(person.hasOwnpropert("name")); // true

console.log("toString" in person); // true
console.log(person.hasOwnproperty("toString")); // false

3.3 刪除屬性

設置一個屬性的值爲 null 並不能從對象中完全移除那個屬性,這只是調用 [[Set]]null 值替換了該屬性原來的值而已。
delete 操做符針對單個對象屬性調用名爲 [[Delete]] 的內部方法。刪除成功時,返回 true

var person = {
    name: "Nicholas"
}

person.name = null;
console.log("name" in person); // true
delete person.name;
console.log(person.name); // undefined 訪問一個不存在的屬性將返回 undefined
console.log("name" in person); // false

3.4 屬性枚舉

全部人爲添加的屬性默認都是可枚舉的。可枚舉的內部特徵 [[Enumerable]] 都被設置爲 true
for-in 循環會枚舉一個對象全部的可枚舉屬性。

個人備註:在Chrome中,對象屬性會按ASCII表排序,而不是定義時的順序。

ECMAScript 5 的 Object() 方法能夠獲取可枚舉屬性的名字的數組。

var person = {
    name: "Ljc",
    age: 18
}

Object.keys(person); // ["name", "age"];

for-inObject.keys() 的一個區別是:前者也會遍歷原型屬性,然後者返回自有(實例)屬性。

實際上,對象的大部分原生方法的 [[Enumerable]] 特徵都被設置爲 false。可用 propertyIsEnumerable() 方法檢查一個屬性是否爲可枚舉的。

var arr = ["abc", 2];
console.log(arr.propertyIsEnumerable("length")); // false

3.5 屬性類型

屬性有兩種類型:數據屬性訪問器屬性
數據屬性包含一個值。[[Put]] 方法的默認行爲是建立數據屬性
訪問器屬性不包含值而是定義了一個當屬性被讀取時調用的函數(稱爲getter)和一個當屬性被寫入時調用的函數(稱爲setter)。訪問器屬性僅須要 gettersetter 二者中的任意一個,固然也能夠二者。

// 對象字面形式中定義訪問器屬性有特殊的語法:
var person = {
    _name: "Nicholas",
    
    get name(){
        console.log("Reading name");
        return this._name;
    },
    set name(value){
        console.log("Setting name to %s", value);
        this._name = value;
    }
};

console.log(person.name); // "Reading name" 而後輸出 "Nicholas"

person.name = "Greg";
console.log(person.name); // "Setting name to Greg" 而後輸出 "Greg"

前置下劃線_ 是一個約定俗成的命名規範,表示該屬性是私有的,實際上它仍是公開的。

訪問器就是定義了咱們在對象讀取或設置屬性時,觸發的動做(函數),_name 至關於一個內部變量。
當你但願賦值(讀取)操做會觸發一些行爲,訪問器就會很是有用。

當只定義getter或setter其一時,該屬性就會變成只讀或只寫。

3.6 屬性特徵

在ECMAScript 5 以前沒有辦法指定一個屬性是否可枚舉。實際上根本沒有方法訪問屬性的任何內部特徵。爲了改變這點,ECMAScript 5引入了多種方法來和屬性特徵值直接互動。

3.6.1 通用特徵

數據屬性和訪問器屬性均由如下兩個屬性特製:
[[Enumerable]] 決定了是否能夠遍歷該屬性;
[[Configurable]] 決定了該屬性是否可配置。

全部人爲定義的屬性默認都是可枚舉、可配置的。

能夠用 Object.defineProperty() 方法改變屬性特徵。
其參數有三:擁有該屬性的對象、屬性名和包含須要設置的特性的屬性描述對象。

var person = {
    name: "Nicholas"
}
Object.defineProperty(person, "name", {
    enumerable: false
})

console.log("name" in person); // true
console.log(person.propertyIsEnumerable("name")); // false

var properties = Object.keys(person);
console.log(properties.length); // 0

Object.defineProperty(person, "name",{
    configurable: false
})

delete person.name; // false
console.log("name" in person); // true

Object.defineProperty(person, "name",{ // error! 
// 在 chrome:Uncaught TypeError: Cannot redefine property: name
    configurable: true
})

沒法將一個不可配置的屬性變爲可配置,相反則能夠。

3.6.2 數據屬性特徵

數據屬性額外擁有兩個訪問器屬性不具有的特徵。
[[Value]] 包含屬性的值(哪怕是函數)。
[[Writable]] 布爾值,指示該屬性是否可寫入。全部屬性默認都是可寫的。

var person = {};

Object.defineProperty(person, "name", {
    value: "Nicholas",
    enumerable: true,
    configurable: true,
    writable: true
})

Object.defineProperty() 被調用時,若是屬性原本就有,則會按照新定義屬性特徵值去覆蓋默認屬性特徵(enumberableconfigurablewritable 均爲 true)。但若是用該方法定義新的屬性時,沒有爲全部的特徵值指定一個值,則全部布爾值的特徵值會被默認設置爲 false。即不可枚舉、不可配置、不可寫的。
當你用 Object.defineProperty() 改變一個已有的屬性時,只有你指定的特徵會被改變。

3.6.3 訪問器屬性特徵

訪問器屬性額外擁有兩個特徵。[[Get]][[Set]],內含 gettersetter 函數。
使用訪問其屬性特徵比使用對象字面形式定義訪問器屬性的優點在於:能夠爲已有的對象定義這些屬性。然後者只能在建立時定義訪問器屬性。

var person = {
    _name: "Nicholas"
};

Object.defineProperty(person, "name", {
    get: function(){
        return this._name;
    },
    set: function(value){
        this._name = value;
    },
    enumerable: true,
    configurable: true
})

for(var x in person){
    console.log(x); // _name \n(換行) name(訪問器屬性)
}

設置一個不可配置、不可枚舉、不能夠寫的屬性:

Object.defineProperty(person, "name",{
    get: function(){
        return this._name;
    }
})

對於一個新的訪問器屬性,沒有顯示設置值爲布爾值的屬性,默認爲 false

3.6.4 定義多重屬性

Object.defineProperties() 方法能夠定義任意數量的屬性,甚至能夠同時改變已有的屬性並建立新屬性。

var person = {};

Object.defineProperties(person, {
    
    // data property to store data
    _name: {
        value: "Nicholas",
        enumerable: true,
        configurable: true,
        writable: true
    },
    
    // accessor property
    name: {
        get: function(){
            return this._name;
        },
        set: function(value){
            this._name = value;
        }
    }
})

3.6.5 獲取屬性特徵

Object.getOwnPropertyDescriptor() 方法。該方法接受兩個參數:對象和屬性名。若是屬性存在,它會返回一個屬性描述對象,內涵4個屬性:configurableenumerable,另外兩個屬性則根據屬性類型決定。

var person = {
    name: "Nicholas"
}

var descriptor = Object.getOwnPropertyDescriptor(person, "name");

console.log(descriptor.enumerable); // true
console.log(descriptor.configuable); // true
console.log(descriptor.value); // "Nicholas"
console.log(descriptor.wirtable); // true

3.7 禁止修改對象

對象和屬性同樣具備指導其行爲的內部特性。其中, [[Extensible]] 是布爾值,指明該對象自己是否能夠被修改。默認是 true。當值爲 false 時,就能禁止新屬性的添加。

建議在 "use strict"; 嚴格模式下進行。

3.7.1 禁止擴展

Object.preventExtensions() 建立一個不可擴展的對象(即不能添加新屬性)。
Object.isExtensible() 檢查 [[Extensible]] 的值。

var person = {
    name: "Nocholas"
}

Object.preventExtensions(person);

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

console.log("sayName" in person); // false

3.7.2 對象封印

一個被封印的對象是不可擴展的且其全部屬性都是不可配置的(即不能添加、刪除屬性或修改其屬性類型(從數據屬性變成訪問器屬性或相反))。只能讀寫它的屬性
Object.seal()。調用此方法後,該對象的 [[Extensible]] 特徵被設置爲 false,其全部屬性的 [[configurable]] 特徵被設置爲 false
Object.isSealed() 判斷一個對象是否被封印。

3.7.3 對象凍結

被凍結的對象不能添加或刪除屬性,不能修改屬性類型,也不能寫入任何數據屬性。簡言而之,被凍結對象是一個數據屬性都爲只讀的被封印對象。
Object.freeze() 凍結對象。
Object.isFrozen() 判斷對象是否被凍結。

3.8 總結

  • in 操做符檢測自有屬性和原型屬性,而 hasOwnProperty() 只檢查自有屬性。

  • delete 操做符刪除對象屬性。

  • 屬性有兩種類型:數據屬性和訪問器屬性。

  • 全部屬性都有一些相關特徵。[[Enumerable]][[Configurable]] 的兩種屬性都有的,而數據屬性還有 [[Value]][[Writable]],訪問器屬性還有 [[Get]][[Set]]。可經過 Object.defineProperty()Object.defineProperties() 改變這些特徵。用 Object.getOwnPropertyDescriptor() 獲取它們。

  • 3 種能夠鎖定對象屬性的方式。

4. 構造函數和原型對象

因爲JavaScript(ES5)缺少類,但可用構造函數和原型對象給對象帶來與類類似的功能。

4.1 構造函數

構造函數的函數名首字母應大寫,以此區分其餘函數。
當沒有須要給構造函數傳遞參數,可忽略小括號:

var Person = {
    // 故意留空
}
var person = new Person;

儘管 Person 構造函數沒有顯式返回任何東西,但 new 操做符會自動建立給定類型的對象並返回它們。

每一個對象在建立時都自動擁有一個構造函數屬性(constructor,實際上是它們的原型對象上的屬性),其中包含了一個指向其構造函數的引用。
經過對象字面量形式({})或Object構造函數建立出來的泛用對象,其構造函數屬性(constructor)指向 Object;而那些經過自定義構造函數建立出來的對象,其構造函數屬性指向建立它的構造函數。

console.log(person.constructor === Person); // true
console.log(({}).constructor === Object); // true
console.log(([1,2,3]).constructor === Object); // true

// 證實 constructor是在原型對象上
console.log(person.hasOwnPrototype("constructor")); // false
console.log(person.constructor.prototype.hasOwnPrototype("constructor")); // true

儘管對象實例及其構造函數之間存在這樣的關係,但仍是建議使用 instanceof 來檢查對象類型。這是由於構造函數屬性能夠被覆蓋。(person.constructor = "")。

當你調用構造函數時,new 會自動自動建立 this 對象,且其類型就是構造函數的類型(構造函數就好像類,至關於一種數據類型)。

你也能夠在構造函數中顯式調用 return。若是返回值是一個對象,它會代替新建立的對象實例而返回,若是返回值是一個原始類型,它會被忽略,新建立的對象實例會被返回。

始終確保要用 new 調用構造函數;不然,你就是在冒着改變全局對象的風險,而不是建立一個新的對象。

var person = Person("Nicholas"); // 缺乏 new

console.log(person instanceof Person); // false
console.log(person); // undefined,由於沒用 new,就至關於一個普通函數,默認返回 undefined
console.log(name); // "Nicholas"

當Person不是被 new 調用時,構造函數中的 this 對象等於全局 this 對象。

在嚴格模式下,會報錯。由於嚴格模式下,並無爲全局對象設置 this,this 保持爲 undefined。

如下代碼,經過 new 實例化 100 個對象,則會有 100 個函數作相同的事。所以可用 prototype 共享同一個方法會更高效。

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

4.2 原型對象

能夠把原型對象看做是對象的基類。幾乎全部的函數(除了一些內建函數)都有一個名爲 prototype 的屬性,該屬性是一個原型對象用來建立新的對象實例。全部建立的對象實例(同一構造函數,固然,可能訪問上層的原型對象)共享該原型對象,且這些對象實例能夠訪問原型對象的屬性。例如,hasOwnProperty()定義在 Object 的原型對象中,但卻可被任何對象看成本身的屬性訪問。

var book = {
    title : "book_name"
}

"hasOwnProperty" in book; // true
book.hasOwnProperty("hasOwnProperty"); // false
Object.property.hasOwnProperty("hasOwnProperty"); // true

鑑別一個原型屬性

function hasPrototypeProperty(object, name){
    return name in object && !object.hasOwnProperty(name);
}

4.2.1 [[Prototype]] 屬性

一個對象實例經過內部屬性 [[Prototype]] 跟蹤其原型對象。該屬性是一個指向該實例使用的原型對象的指針。當你用 new 建立一個新的對象時,構造函數的原型對象就會被賦給該對象的 [[Prototype]] 屬性。

prototype

由上圖能夠看出,[[Prototype]] 屬性是如何讓多個對象實例引用同一個原型對象來減小重複代碼。

Object.getPrototypeOf() 方法可讀取 [[Prototype]] 屬性的值。

var obj = {};
var prototype = Object.getPrototypeOf(Object);

console.log(prototype === Object.prototype); // true

大部分JavaScript引擎在全部對象上都支持一個名爲 _proto_ 的屬性。該屬性使你能夠直接讀寫 [[Prototype]] 屬性。

isPrototypeOf() 方法會檢查某個對象是不是另外一個對象的原型對象,該方法包含在全部對象中。

var obj = {}
console.log(Object.prototype.isPrototypeOf(obj)); // true

當讀取一個對象的屬性時,JavaScript 引擎首先在該對象的自有屬性查找屬性名。若是找到則返回。不然會搜索 [[Prototype]] 中的對象,找到則返回,找不到則返回 undefined。

var obj = new Object();
console.log(obj.toString()); // "[object Object]"

obj.toString = function(){
    return "[object Custom]";
}
console.log(obj.toString()); // "[object Custom]"

delete obj.toString; // true
console.log(obj.toString()); // "[object Object]"

delete obj.toString; // 無效,delete不能刪除一個對象從原型繼承而來的屬性
cconsole.log(obj.toString()); // // "[object Object]"

MDN:delete 操做符不能刪除的屬性有:①顯式聲明的全局變量不能被刪除,該屬性不可配置(not configurable); ②內置對象的內置屬性不能被刪除; ③不能刪除一個對象從原型繼承而來的屬性(不過你能夠從原型上直接刪掉它)。

一個重要概念:沒法給一個對象的原型屬性賦值。我認爲是沒法直接添加吧,在chrome和Edge中,都沒法讀取_proto_屬性,但咱們能夠經過 obj.constructor.prototype.sayHi = function(){console.log("Hi!")} 向原型對象添加屬性。

此處輸入圖片的描述
(圖片中間能夠看出,爲對象obj添加的toString屬性代替了原型屬性)

4.2.2 在構造函數中使用原型對象

在原型對象上定義公用方法
在原型對象上定義數據類型

開發中須要注意原型對象的數據是否共享。

function Person(name){
    this.name = name
}

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

Person.prototype.position = "school";
Person.prototype.arr = [];

var person1 = new Person("xiaoming");
var person2 = new Person("Jc");

console.log("原始類型")
console.log(person1.position); // "school"
console.log(person2.position); // "school"

person1.position = 2; // 這是在當前屬性設置position,引用類型同理
console.log(person1.hasOwnProperty("position")); // true
console.log(person2.hasOwnProperty("position")); // false

console.log("引用類型");
person1.arr.push("pizza"); // 這是在原型對象上設置,而不是直接在對象上
person2.arr.push("quinoa"); // 這是在原型對象上設置
console.log(person1.hasOwnProperty("arr")); // false
console.log(person2.hasOwnProperty("arr")); // false
console.log(person1.arr); // ["pizza", "quinoa"]
console.log(person2.arr); // ["pizza", "quinoa"]

上面是在原型對象上一一添加屬性,下面一種更簡潔的方式:以一個對象字面形式替換原型對象

function Person(name){
    this.name
}

Person.prototype = {
    sayName: function(){
        console.log(this.name);
    },
    toString: function(){
        return "[Person ]" + this.name + "]";
    }
}

這種方式有一種反作用:由於原型對象上具備一個 constructor 屬性,這是其餘對象實例所沒有的。當一個函數被建立時,它的 prototype 屬性也會被建立,且該原型對象的 constructor 屬性指向該函數。當使用字面量時,因沒顯式設置原型對象的 constructor 屬性,所以其 constructor 屬性是指向 Object 的。
所以,當經過此方式設置原型對象時,可手動設置 constructor 屬性。

function Person(name){
    this.name
}

// 建議第一個屬性就是設置其 constructor 屬性。
Person.prototype = {
    constructor: Person,

    sayName: function(){
        console.log(this.name);
    },
    toString: function(){
        return "[Person ]" + this.name + "]";
    }
}

構造函數、原型對象和對象實例之間的關係最有趣的一方面也許是:
對象實例和構造函數直接沒有直接聯繫。(對象實例只有 [[Prototype]] 屬性(本身測試時不能讀取(_proto_))指向其相應的原型對象,而原型對象的 constructor 屬性指向構造函數,而構造函數的 prototype 指向原型對象)
obj_constructor_prototype

4.2.3 改變原型對象

由於每一個對象的 [[Prototype]] 只是一個指向原型對象的指針,因此原型對象的改動會馬上反映到全部引用它的對象。
當對一個對象使用封印 Object.seal() 或凍結 Object.freeze() 時,徹底是在操做對象的自有屬性,但任然能夠經過在原型對象上添加屬性來擴展這些對象實例。

4.2.4 內建對象(如Array、String)的原型對象

String.prototype.capitalize = function(){
    return this.charAt(0).toUpperCase() + this.substring(1);
}

總結

  • 構造函數就是用 new 操做符調用的普通函數。可用過 instanceof 操做符或直接訪問 constructor(其實是原型對象的屬性) 來鑑別對象是被哪一個構造函數所建立的。

  • 每一個函數都有一個 prototype 對象,它定義了該構造函數建立的全部對象共享的屬性。而 constructor 屬性其實是定義在原型對象裏,供全部對象實例共享。

  • 每一個對象實例都有 [[Prototype]] 屬性,它是指向原型對象的指針。當訪問對象的某個屬性時,先從對象自身查找,找不到的話就到原型對象上找。

  • 內建對象的原型對象也可被修改

5. 繼承

5.1 原型對象鏈和 Object.prototype

JavaScript內建的繼承方法被稱爲 原型對象鏈(又叫原型對象繼承)。
原型對象的屬性可經由對象實例訪問,這就是繼承的一種形式。對象實例繼承了原型對象的屬性,而原型對象也是一個對象,它也有本身的原型對象並繼承其屬性,以此類推。這就是原型對象鏈。

全部對象(包括自義定的)都自動繼承自 Object,除非你另有指定。更確切地說,全部對象都繼承自 Object.prototype。任何以對象字面量形式定義的對象,其 [[Prototype]] 的值都被設爲 Object.prototype,這意味着它繼承 Object.prototype 的屬性。

5.1.1 繼承自 Object.prototype 的方法

Object.prototype 通常有如下幾個方法

  • hasOwnProperty() 檢測是否存在一個給定名字的自有屬性

  • propertyIsemumerable() 檢查一個自有屬性是否可枚舉

  • isPrototypeOf 檢查一個對象是不是另外一個對象的原型對象

  • valueOf() 返回一個對象的值表達

  • toString() 返回一個對象的字符串表達

這 5 種方法經由繼承出如今全部對象中。
由於全部對象都默認繼承自 Object.prototype,因此改變它就會影響全部的對象。因此不建議。

5.2 繼承

對象繼承是最簡單的繼承類型。你惟須要作的是指定哪一個對象是新對象的 [[Prototype]]。對象字面量形式會隱式指定 Object.prototype 爲其 [[Protoype]]。固然咱們能夠用 ES5 的 Object.create() 方法顯式指定。該方法接受兩個參數,第一個是新對象的的 [[Prototype]] 所指向的對象。第二個參數是可選的一個屬性描述對象,其格式與 Object.definePrototies()同樣。

var obj = {
    name: "Ljc"
};

// 等同於
var obj = Object.create(Object.prototype, {
    name: {
        value: "Ljc",
        configurable: true,
        enumberable: true,
        writable: true
    }
});

下面是繼承其它對象:

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

對象繼承

當訪問一個對象屬性時,JavaScript引擎會執行一個搜索過程。若是在對象實例存在該自有屬性,則返回,不然,根據其私有屬性 [[Protoype]] 所指向的原型對象進行搜索,找到返回,不然繼承上述操做,知道繼承鏈末端。末端一般是 Object.prototype,其 [[Prototype]]null

固然,也能夠用 Object.create() 常見一個 [[Prototype]]null 的對象。

var obj = Object.create(null);

console.log("toString" in obj); // false

該對象是一個沒有原型對象鏈的對象,便是一個沒有預約義屬性的白板。

5.3 構造函數繼承

JavaScript 中的對象繼承也是構造函數繼承的基礎。
第四章提到,幾乎全部函數都有 prototype 屬性,它可被修改或替換。該 prototype 屬性被自動設置爲一個新的繼承自 Object.prototype 的泛用對象,該對象(原型對象)有一個自有屬性 constructor。實際上,JavaScript 引擎爲你作了下面的事情。

// 你寫成這樣
function YourConstructor(){
    // initialization
}

// JavaScript引擎在背後爲你作了這些處理
YourConstructor.prototype = Object.create(Object.prototype, {
    constructor: {
        configurable: true,
        enumerable: true,
        value: YourConstructor,
        writable: true
    }
})

你不須要作額外的工做,這段代碼幫你把構造函數的 prototype 屬性設置爲一個繼承自 Object.prototype 的對象。這意味着 YourConstructor 建立出來的任何對象都繼承自 Object.prototype

因爲 prototype 可寫,你能夠經過改變它來改變原型對象鏈。

MDN:instanceof 運算符能夠用來判斷某個構造函數的 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 + "]";
}
// 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

構造函數繼承

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中的繼承是經過原型對象鏈來實現的,所以不須要調用對象的父類的構造函數。若是確實須要在子類構造函數中調用父類構造函數,那就能夠在子類的構造函數中利用 callapply方法調用父類的構造函數。

// 在上面的代碼基礎上做出修改
// inherits from Rectangle
function Square(size){
    Rectangle.call(this, size, size);
    
    // optional: add new properties or override existing ones here
}

通常來講,須要修改 prototyp 來繼承方法並用構造函數竊取來設置屬性,因爲這種作法模仿了那些基於類的語言的類繼承,因此這一般被稱爲僞類繼承。

5.5 訪問父類方法

其實也是經過指定 callapply 的子對象調用父類方法。

6 對象模式

6.1 私有成員和特權成員

JavaScipt 對象的全部屬性都是公有的,沒有顯式的方法指定某個屬性不能被外界訪問。

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 agel
        }
    }
}());

// 暴露模塊模式
var yourObj = (function(){
    var age = 25;
    function getAge(){
        return agel
    };
    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

這裏有個問題:若是你須要對象實例擁有私有數據,就不能將相應方法放在 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];
        }
    }
}

這是淺拷貝,若是屬性的值是一個引用,那麼二者將指向同一個對象。

6.3 做用域安全的構造函數

構造函數也是函數,因此不用 new 也能調用它們來改變 this 的值。在非嚴格模式下, this 被強制指向全局對象。而在嚴格模式下,構造函數會拋出一個錯誤(由於嚴格模式下沒有爲全局對象設置 thisthis 保持爲 undefined)。
而不少內建構造函數,例如 ArrayRegExp 不須要 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);
    }
}

總結

看了兩天的書,作了兩天的筆記。固然這只是ES5的。過幾天 ES6 新書又來了。最後感謝 異步社區 送我這本好書 《JavaScript面向對象精要》,讓個人前端根基更加穩固,但願本身的前端之路越走越順。

對應 GitHub

相關文章
相關標籤/搜索