摘要:解除引用的最佳手段是將對象變量設置為。字面形式允許你在不需要使用操作符和構造函數顯示創建對象的情況下生成引用值。函數就是值可以像使用對象一樣使用函數因為函數本來就是對象,構造函數更加容易說明。
JavaScript(ES5)的面向對象精要
標簽: JavaScript 面向對象 讀書筆記
2016年1月16日-17日兩天看完了《JavaScript面向對象精要》(參加異步社區的活動送的),這本書雖然不夠100頁,但都是精華,不愧是《JavaScript高級程序設計》作者 Nicholas C.Zakas 的最新力作。
下面是我的讀書筆記(ES5):
1.原始類型和引用類型 1.1 什么是類型原始類型 保存為簡單數據值。
引用類型 保存為對象,其本質是指向內存位置的引用。
為了讓開發者能夠把原始類型和引用類型按相同的方式處理,JavaScript花費了很大的努力來保證語言的一致性。
其他編程語言用棧存原始類型,用對存儲引用類型。而JavaScript則完全不同:它使用一個變量對象追蹤變量的生存期。原始值被直接保存在變量對象內,而引用值則作為一個指針保存在變量對象內,該指針指向實際對象在內存中的存儲位置。
1.2 原始類型原始類型代表照原樣保存的一些簡單數據。
JavaScript共有 5 種原始類型:
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"
1.3 引用類型盡管原始類型擁有方法,但它們不是對象。JavaScript使它們看上去像對象一樣,以此來提高語言上的一致性體驗。
引用類型是指JavaScript中的對象,同時也是你在該語言中能找到最接近類的東西。
引用值是引用類型的實例,也是對象的同義詞(后面將用對象指代引用值)。對象是屬性的無序列表。屬性包含鍵(始終是字符串)和值。如果一個屬性的值是函數,它就被稱為方法。除了函數可以運行以外,一個包含數組的屬性和一個包含函數的屬性沒有什么區別。
有時候,把JavaScript對象想象成哈希表可以幫助你更好地理解對象結構。
JavaScript 有好幾種方法可以創建對象,或者說實例化對象。第一種是使用 new 操作符和構造函數。
構造函數就是通過 new 操作符來創建對象的函數——任何函數都可以是構造函數。根據命名規范,JavaScript中的構造函數用首字母大寫來跟非構造函數進行區分。
var object = new Object();
因為引用類型不再變量中直接保存對象,所以本例中的 object 變量實際上并不包含對象的實例,而是一個指向內存中實際對象所在位置的指針(或者說引用)。這是對象和原始值之間的一個基本差別,原始值是直接保存在變量中。
當你將一個對象賦值給變量時,實際是賦值給這個變量一個指針。這意味著,將一個變量賦值給另外一個變量時,兩個變量各獲得了一份指針的拷貝,指向內存中的同一個對象。
var obj1 = new Object(); var obj2 = obj1;對象引用解除
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;
1.5 訪問屬性雖然使用字面形式并沒有調用 new Object(),但是JavaScript引擎背后做的工作和 new Object()一樣,除了沒有調用構造函數。其他引用類型的字面形式也是如此。
可通過 . 和 中括號 訪問對象的屬性。
中括號[]在需要動態決定訪問哪個屬性時,特別有用。因為你可以用變量而不是字符串字面形式來指定訪問的屬性。
函數是最容易鑒別的引用類型,因為對函數使用 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。
1.8 原始封裝類型雖然 instanceof 可以鑒別對象類型(如數組),但是有一個列外。JavaScript 的值可以在同一個網頁的不用框架之間傳來傳去。由于每個網頁擁有它自己的全局上下文——Object、Array以及其他內建類型的版本。所以當你把一個對象(如數組)從一個框架傳到另外一個框架時,instanceof就無法識別它。
原始封裝類型有 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;
1.9 總結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.
第一章的東西都是我們一些比較熟悉的知識。但是也有一些需要注意的地方:
正確區分原始類型和引用類型
對于 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 參數函數參數保存在類數組對象 argument (Array.isArray(arguments) 返回 false)中。可以接收任意數量的參數。
函數的 length 屬性表明其期望的參數個數。
大多數面向對象語言支持函數重載,它能讓一個函數具有多個簽名。函數簽名由函數的名字、參數的個數及其類型組成。
而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 值。
fun.call(thisArg[, arg1[, arg2[, ...]]]);
fun.apply(thisArg, [argsArray]);
fun.bind(thisArg[, arg1[, arg2[, ...]]])
使用 call 或 apply 方法,就不需要將函數加入每個對象——你顯示地指定了 this 的值而不是讓JavaScript引擎自動指定。
call 與 apply 的不同地方是,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]] 的方法。
檢查對象是否已有一個屬性。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")); // false3.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); // false3.4 屬性枚舉
所有人為添加的屬性默認都是可枚舉的。可枚舉的內部特征 [[Enumerable]] 都被設置為 true。
for-in 循環會枚舉一個對象所有的可枚舉屬性。
我的備注:在Chrome中,對象屬性會按ASCII表排序,而不是定義時的順序。
ECMAScript 5 的 Object() 方法可以獲取可枚舉屬性的名字的數組。
var person = { name: "Ljc", age: 18 } Object.keys(person); // ["name", "age"];
for-in 與 Object.keys() 的一個區別是:前者也會遍歷原型屬性,而后者返回自有(實例)屬性。
實際上,對象的大部分原生方法的 [[Enumerable]] 特征都被設置為 false。可用 propertyIsEnumerable() 方法檢查一個屬性是否為可枚舉的。
var arr = ["abc", 2]; console.log(arr.propertyIsEnumerable("length")); // false3.5 屬性類型
屬性有兩種類型:數據屬性和訪問器屬性。
數據屬性包含一個值。[[Put]] 方法的默認行為是創建數據屬性。
訪問器屬性不包含值而是定義了一個當屬性被讀取時調用的函數(稱為getter)和一個當屬性被寫入時調用的函數(稱為setter)。訪問器屬性僅需要 getter 或 setter 兩者中的任意一個,當然也可以兩者。
// 對象字面形式中定義訪問器屬性有特殊的語法: 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 相當于一個內部變量。
當你希望賦值(讀取)操作會觸發一些行為,訪問器就會非常有用。
3.6 屬性特征當只定義getter或setter其一時,該屬性就會變成只讀或只寫。
在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() 被調用時,如果屬性本來就有,則會按照新定義屬性特征值去覆蓋默認屬性特征(enumberable、configurable 和 writable 均為 true)。但如果用該方法定義新的屬性時,沒有為所有的特征值指定一個值,則所有布爾值的特征值會被默認設置為 false。即不可枚舉、不可配置、不可寫的。
當你用 Object.defineProperty() 改變一個已有的屬性時,只有你指定的特征會被改變。
訪問器屬性額外擁有兩個特征。[[Get]] 和 [[Set]],內含 getter 和 setter 函數。
使用訪問其屬性特征比使用對象字面形式定義訪問器屬性的優勢在于:可以為已有的對象定義這些屬性。而后者只能在創建時定義訪問器屬性。
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 (換行) 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個屬性:configurable 和 enumerable,另外兩個屬性則根據屬性類型決定。
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); // true3.7 禁止修改對象
對象和屬性一樣具有指導其行為的內部特性。其中, [[Extensible]] 是布爾值,指明該對象本身是否可以被修改。默認是 true。當值為 false 時,就能禁止新屬性的添加。
3.7.1 禁止擴展建議在 "use strict"; 嚴格模式下進行。
Object.preventExtensions() 創建一個不可擴展的對象(即不能添加新屬性)。
Object.isExtensible() 檢查 [[Extensible]] 的值。
var person = { name: "Nocholas" } Object.preventExtensions(person); person.sayName = function(){ console.log(this.name) } console.log("sayName" in person); // false3.7.2 對象封印
一個被封印的對象是不可擴展的且其所有屬性都是不可配置的(即不能添加、刪除屬性或修改其屬性類型(從數據屬性變成訪問器屬性或相反))。只能讀寫它的屬性。
Object.seal()。調用此方法后,該對象的 [[Extensible]] 特征被設置為 false,其所有屬性的 [[configurable]] 特征被設置為 false。
Object.isSealed() 判斷一個對象是否被封印。
被凍結的對象不能添加或刪除屬性,不能修改屬性類型,也不能寫入任何數據屬性。簡言而之,被凍結對象是一個數據屬性都為只讀的被封印對象。
Object.freeze() 凍結對象。
Object.isFrozen() 判斷對象是否被凍結。
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]] 屬性是如何讓多個對象實例引用同一個原型對象來減少重復代碼。
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屬性代替了原型屬性)
開發中需要注意原型對象的數據是否共享。
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 指向原型對象)
因為每個對象的 [[Prototype]] 只是一個指向原型對象的指針,所以原型對象的改動會立刻反映到所有引用它的對象。
當對一個對象使用封印 Object.seal() 或凍結 Object.freeze() 時,完全是在操作對象的自有屬性,但任然可以通過在原型對象上添加屬性來擴展這些對象實例。
String.prototype.capitalize = function(){ return this.charAt(0).toUpperCase() + this.substring(1); }總結
構造函數就是用 new 操作符調用的普通函數。可用過 instanceof 操作符或直接訪問 constructor(實際上是原型對象的屬性) 來鑒別對象是被哪個構造函數所創建的。
每個函數都有一個 prototype 對象,它定義了該構造函數創建的所有對象共享的屬性。而 constructor 屬性實際上是定義在原型對象里,供所有對象實例共享。
每個對象實例都有 [[Prototype]] 屬性,它是指向原型對象的指針。當訪問對象的某個屬性時,先從對象自身查找,找不到的話就到原型對象上找。
內建對象的原型對象也可被修改
5. 繼承 5.1 原型對象鏈和 Object.prototypeJavaScript內建的繼承方法被稱為 原型對象鏈(又叫原型對象繼承)。
原型對象的屬性可經由對象實例訪問,這就是繼承的一種形式。對象實例繼承了原型對象的屬性,而原型對象也是一個對象,它也有自己的原型對象并繼承其屬性,以此類推。這就是原型對象鏈。
所有對象(包括自義定的)都自動繼承自 Object,除非你另有指定。更確切地說,所有對象都繼承自 Object.prototype。任何以對象字面量形式定義的對象,其 [[Prototype]] 的值都被設為 Object.prototype,這意味著它繼承 Object.prototype 的屬性。
5.1.1 繼承自 Object.prototype 的方法Object.prototype 一般有以下幾個方法
hasOwnProperty() 檢測是否存在一個給定名字的自有屬性
propertyIsemumerable() 檢查一個自有屬性是否可枚舉
isPrototypeOf 檢查一個對象是否是另一個對象的原型對象
valueOf() 返回一個對象的值表達
toString() 返回一個對象的字符串表達
這 5 種方法經由繼承出現在所有對象中。
因為所有對象都默認繼承自 Object.prototype,所以改變它就會影響所有的對象。所以不建議。
對象繼承是最簡單的繼承類型。你唯需要做的是指定哪個對象是新對象的 [[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中的繼承是通過原型對象鏈來實現的,因此不需要調用對象的父類的構造函數。如果確實需要在子類構造函數中調用父類構造函數,那就可以在子類的構造函數中利用 call、apply方法調用父類的構造函數。
// 在上面的代碼基礎上作出修改 // inherits from Rectangle function Square(size){ Rectangle.call(this, size, size); // optional: add new properties or override existing ones here }
一般來說,需要修改 prototyp 來繼承方法并用構造函數竊取來設置屬性,由于這種做法模仿了那些基于類的語言的類繼承,所以這通常被稱為偽類繼承。
5.5 訪問父類方法其實也是通過指定 call 或 apply 的子對象調用父類方法。
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()); // 236.2 混入
這是一種偽繼承。一個對象在不改變原型對象鏈的情況下得到了另外一個對象的屬性被稱為“混入”。因此,和繼承不同,混入讓你在創建對象后無法檢查屬性來源。
純函數實現:
function mixin(receiver, supplier){ for(var property in supplier){ if(supplier.hasOwnProperty(property)){ receiver[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); } }總結
看了兩天的書,做了兩天的筆記。當然這只是ES5的。過幾天 ES6 新書又來了。最后感謝 異步社區 送我這本好書 《JavaScript面向對象精要》,讓我的前端根基更加穩固,希望自己的前端之路越走越順。
對應 GitHub。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/78521.html
摘要:面向對象精要讀書筆記下面代碼的實際執行過程是什么使用原始值和原始封裝類型是有區別的因為是被解析成一個對象的,所以肯定是真的函數是對象,函數有兩種字面形式,第一種是函數聲明,以關鍵字開頭后面跟函數名字。 Javascript面向對象精要讀書筆記 1、下面代碼的實際執行過程是什么? var name = fan var str = name.charAt(0) console.l...
摘要:使用時,會自動創建對象,其類型為構造函數類型,指向對象實例缺少關鍵字,指向全局對象。構造函數本身也具有屬性指向原型對象。 在JavaScript面向對象精要(一)中講解了一些與面向對象相關的概念和方法,這篇講講原型和繼承。 構造函數和原型對象 構造函數也是函數,用new創建對象時調用的函數,與普通函數的一個區別是,其首字母應該大寫。但如果將構造函數當作普通函數調用(缺少new關鍵字...
摘要:使函數不同于其他對象的決定性特性是函數存在一個被稱為的內部屬性。其中,是一個布爾值,指明改對象本身是否可以被修改值為。注意凍結對象和封印對象均要在嚴格模式下使用。 數據類型 在JavaScript中,數據類型分為兩類: 原始類型:保存一些簡單數據,如true,5等。JavaScript共有5中原始類型: boolean:布爾,值為true或false number:數字,值...
摘要:原文第一章主要介紹的大概情況基本語法。通過和來引用對象屬性或數組元素的值就構成一個表達式。 原文:https://keelii.github.io/2016/06/16/javascript-definitive-guide-note-0/ 第一章 主要介紹 JavaScript 的大概情況、基本語法。之前沒有 JavaScript 基礎的看不懂也沒關系,后續章節會有進一步的詳細說明...
閱讀 2111·2021-11-24 10:28
閱讀 1117·2021-10-12 10:12
閱讀 3337·2021-09-22 15:21
閱讀 679·2021-08-30 09:44
閱讀 1895·2021-07-23 11:20
閱讀 1147·2019-08-30 15:56
閱讀 1751·2019-08-30 15:44
閱讀 1483·2019-08-30 13:55