- 原文地址:Let’s explore objects in JavaScript
- 原文作者:Cristi Salcescu
- 譯文出自:阿里雲翻譯小組
- 譯文連結:github.com/dawn-teams/…
- 譯者:靈沼
- 校對者:也樹,眠雲
一起探討 JavaScript 的物件
物件是多個屬性的動態集合,它有一個連結著原型的隱藏屬性(注:__proto__
)。
一個屬性擁有一個 key 和一個 value 。
屬性的 key
屬性的 key 是一個唯一的字串。
訪問屬性有兩種方式:點表示法和括號表示法。當使用點表示法,屬性的 key 必須是有效的識別符號。
let obj = {
message : "A message"
}
obj.message //"A message"
obj["message"] //"A message"
複製程式碼
訪問一個不存在的屬性不會丟擲錯誤,但是會返回 undefined
。
obj.otherProperty //undefined
複製程式碼
當使用括號表示法,屬性的 key 不要求是有效的識別符號 —— 可以是任意值。
let french = {};
french["thank you very much"] = "merci beaucoup";
french["thank you very much"]; //"merci beaucoup"
複製程式碼
當屬性的key是一個非字串的值,會用toString()方法(如果可用的話)把它轉換為字串。
let obj = {};
//Number
obj[1] = "Number 1";
obj[1] === obj["1"]; //true
//Object
let number1 = {
toString : function() { return "1"; }
}
obj[number1] === obj["1"]; //true
複製程式碼
在上面的示例中,物件 number1
被用作一個 key 。它會被轉換為字串,轉換結果 “1” 被用作屬性的 key 。
屬性的值
屬性的值可以是任意的基礎資料型別,物件,或函式。
物件作為值
物件可以巢狀在其他物件裡。看下面這個例子:
let book = {
title : "The Good Parts",
author : {
firstName : "Douglas",
lastName : "Crockford"
}
}
book.author.firstName; //"Douglas"
複製程式碼
通過這種方式,我們就可以建立一個名稱空間:
let app = {};
app.authorService = { getAuthors : function() {} };
app.bookService = { getBooks : function() {} };
複製程式碼
函式作為值
當一個函式被作為屬性值,通常成為一個方法。在方法中,this
關鍵字代表著當前的物件。
this
,會根據函式的呼叫方式有不同的值。瞭解更多關於this
丟失上下文的問題,可以檢視當”this”丟失上下文時應該怎麼辦。
動態性
物件本質上就是動態的。可以任意新增刪除屬性。
let obj = {};
obj.message = "This is a message"; //add new property
obj.otherMessage = "A new message"; //add new property
delete obj.otherMessage; //delete property
複製程式碼
Map
我們可以把物件當做一個 Map。Map 的 key 就是物件的屬性。
訪問一個 key 不需要去掃描所有屬性。訪問的時間複雜度是 o(1)。
原型
物件有一個連結著原型物件的“隱藏”屬性 __proto__
,物件是從這個原型物件中繼承屬性的。
舉個例子,使用物件字面量建立的物件有一個指向 Object.prototype
的連結:
var obj = {};
obj.__proto__ === Object.prototype; //true
複製程式碼
原型鏈
原型物件有它自己的原型。當一個屬性被訪問的時候並且不包含在當前物件中,JavaScript會沿著原型鏈向下查詢直到找到被訪問的屬性,或者到達 null
為止。
只讀
原型只用於讀取值。物件進行更改時,只會作用到當前物件,不會影響物件的原型;就算原型上有同名的屬性,也是如此。
空物件
正如我們看到的,空物件 {}
並不是真正意義上的空,因為它包含著指向 Object.prototype
的連結。為了建立一個真正的空物件,我們可以使用 Object.create(null)
。它會建立一個沒有任何屬性的物件。這通常用來建立一個Map。
原始值和包裝物件
在允許訪問屬性這一點上,JavaScript 把原始值描述為物件。當然了,原始值並不是物件。
(1.23).toFixed(1); //"1.2"
"text".toUpperCase(); //"TEXT"
true.toString(); //"true"
複製程式碼
為了允許訪問原始值的屬性, JavaScript 創造了一個包裝物件,然後銷燬它。JavaScript引擎對建立包裝和銷燬包裝物件的過程做了優化。
數值、字串和布林值都有等效的包裝物件。跟別是:Number
、String
、Boolean
。
null
和 undefined
原始值沒有相應的包裝物件並且不提供任何方法。
內建原型
Numbers 繼承自Number.prototype
,Number.prototype
繼承自Object.prototype
。
var no = 1;
no.__proto__ === Number.prototype; //true
no.__proto__.__proto__ === Object.prototype; //true
複製程式碼
Strings 繼承自 String.prototype
。Booleans 繼承自 Boolean.prototype
函式都是物件,繼承自 Function.prototype
。函式擁有 bind()
、apply()
和 call()
等方法。
所有物件、函式和原始值(除了 null
和 undefined
)都從 Object.prototype
繼承屬性。他們都有 toString()
方法。
使用 polyfill 擴充內建物件
JavaScript 可以輕鬆地使用新功能擴充內建物件。
polyfill 就是一個程式碼片段,用於在不支援某功能的瀏覽器中實現該功能。
實用工具
舉個例子,這個為 Object.assign()
寫的polyfill,如果它不可用,那麼就在 Object
上新增一個新方法。
為 Array.from()
寫了類似的polyfill,如果它不可用,就在 Array
上新增一個新方法。
原型
新的方法可以被新增到原型。
舉個例子,String.prototype.trim()
polyfill讓所有的字串都能使用 trim()
方法。
let text = " A text ";
text.trim(); //"A text"
複製程式碼
Array.prototype.find()
polyfill讓所有的陣列都能使用find()
方法。polyfill也是同樣的。
let arr = ["A", "B", "C", "D", "E"];
arr.indexOf("C"); //2
複製程式碼
單一繼承
Object.create()
用特定的原型物件建立一個新物件。它用來做單一繼承。思考下面的例子:
let bookPrototype = {
getFullTitle : function(){
return this.title + " by " + this.author;
}
}
let book = Object.create(bookPrototype);
book.title = "JavaScript: The Good Parts";
book.author = "Douglas Crockford";
book.getFullTitle();//JavaScript: The Good Parts by Douglas Crockford
複製程式碼
多重繼承
Object.assign()
從一個或多個物件拷貝屬性到目標物件。它用來做多重繼承。看下面的例子:
let authorDataService = { getAuthors : function() {} };
let bookDataService = { getBooks : function() {} };
let userDataService = { getUsers : function() {} };
let dataService = Object.assign({},
authorDataService,
bookDataService,
userDataService
);
dataService.getAuthors();
dataService.getBooks();
dataService.getUsers();
複製程式碼
不可變物件
Object.freeze()
凍結一個物件。屬性不能被新增、刪除、更改。物件會變成不可變的。
"use strict";
let book = Object.freeze({
title : "Functional-Light JavaScript",
author : "Kyle Simpson"
});
book.title = "Other title";//Cannot assign to read only property `title`
複製程式碼
Object.freeze()
實行淺凍結。要深凍結,需要遞迴凍結物件的每一個屬性。
拷貝
Object.assign()
被用作拷貝物件。
let book = Object.freeze({
title : "JavaScript Allongé",
author : "Reginald Braithwaite"
});
let clone = Object.assign({}, book);
複製程式碼
Object.assign()
執行淺拷貝,不是深拷貝。它拷貝物件的第一層屬性。巢狀的物件會在原始物件和副本物件之間共享。
物件字面量
物件字面量提供一種簡單、優雅的方式建立物件。
let timer = {
fn : null,
start : function(callback) { this.fn = callback; },
stop : function() {},
}
複製程式碼
但是,這種語法有一些缺點。所有的屬性都是公共的,方法能夠被重定義,並且不能在新例項中使用相同的方法。
timer.fn;//null
timer.start = function() { console.log("New implementation"); }
複製程式碼
Object.create()
Object.create()
和 Object.freeze()
一起能夠解決最後兩個問題。
首先,我要使用所有方法建立一個凍結原型 timerPrototype
,然後建立物件去繼承它。
let timerPrototype = Object.freeze({
start : function() {},
stop : function() {}
});
let timer = Object.create(timerPrototype);
timer.__proto__ === timerPrototype; //true
複製程式碼
當原型被凍結,繼承它的物件不能夠更改其中的屬性。現在,start()
和 stop()
方法不能被重新定義。
"use strict";
timer.start = function() { console.log("New implementation"); } //Cannot assign to read only property `start` of object
複製程式碼
Object.create(timerPrototype)
可以用來使用相同的原型構建更多物件。
建構函式
最初,JavaScript 語言提出建構函式作為這些的語法糖。看下面的程式碼:
function Timer(callback){
this.fn = callback;
}
Timer.prototype = {
start : function() {},
stop : function() {}
}
function getTodos() {}
let timer = new Timer(getTodos);
複製程式碼
所有的以 function
關鍵字定義的函式都可以作為建構函式。建構函式使用功能 new
呼叫。新物件將原型設定為 FunctionConstructor.prototype
。
let timer = new Timer();
timer.__proto__ === Timer.prototype;
複製程式碼
同樣地,我們需要凍結原型來防止方法被重定義。
Timer.prototype = Object.freeze({
start : function() {},
stop : function() {}
});
複製程式碼
new 操作符
當執行 new Timer()
時,它與函式 newTimer()
作用相同:
function newTimer(){
let newObj = Object.create(Timer.prototype);
let returnObj = Timer.call(newObj, arguments);
if(returnObj) return returnObj;
return newObj;
}
複製程式碼
使用 Timer.prototype
作為原型,創造了一個新物件。然後執行 Timer
函式併為新物件設定屬性欄位。
類
ES2015為這一切帶來了更好的語法糖。看下面的例子:
class Timer{
constructor(callback){
this.fn = callback;
}
start() {}
stop() {}
}
Object.freeze(Timer.prototype);
複製程式碼
使用 class 構建的物件將原型設定為 ClassName.prototype
。在使用類建立物件時,必須使用 new
操作符。
let timer= new Timer();
timer.__proto__ === Timer.prototype;
複製程式碼
class 語法不會凍結原型,所以我們需要在之後進行操作。
Object.freeze(Timer.prototype);
複製程式碼
基於原型的繼承
在 JavaScript 中,物件繼承自物件。
建構函式和類都是用來建立原型物件的所有方法的語法糖。然後它建立一個繼承自原型物件的新物件,併為新物件設定資料欄位
基於原型的繼承具有保護記憶的好處。原型只建立一次並且由所有的例項使用。
沒有封裝
基於原型的繼承模式沒有私有性。所有物件的屬性都是公有的。
Object.keys()
返回一個包含所有屬性鍵的陣列。它可以用來迭代物件的所有屬性。
function logProperty(name){
console.log(name); //property name
console.log(obj[name]); //property value
}
Object.keys(obj).forEach(logProperty);
複製程式碼
模擬的私有模式包含使用 _
來標記私有屬性,這樣其他人會避免使用他們:
class Timer{
constructor(callback){
this._fn = callback;
this._timerId = 0;
}
}
複製程式碼
工廠模式
JavaScript 提供一種使用工廠模式建立封裝物件的新方式。
function TodoStore(callback){
let fn = callback;
function start() {},
function stop() {}
return Object.freeze({
start,
stop
});
}
複製程式碼
fn
變數是私有的。只有 start()
和 stop()
方法是公有的。start()
和 stop()
方法不能被外界改變。這裡沒有使用 this
,所以沒有 this
丟失上下文的問題。
物件字面量依然用於返回物件,但是這次它只包含函式。更重要的是,這些函式是共享相同私有狀態的閉包。 Object.freeze()
被用來凍結公有 API。
Timer 物件的完整實現,請看具有封裝功能的實用JavaScript物件.
結論
JavaScript 像物件一樣處理原始值、物件和函式。
物件本質上是動態的,可以用作 Map。
物件繼承自其他物件。建構函式和類是建立從其他原型物件繼承的物件的語法糖。
Object.create()
可以用來單一繼承,Object.assign()
用來多重繼承。
工廠函式可以構建封裝物件。
有關 JavaScript 功能的更多資訊,請看:
Discover the power of first class functions
How point-free composition will make you a better functional programmer
Here are a few function decorators you can write from scratch