「譯」一起探討 JavaScript 的對象
「譯」一起探討 JavaScript 的對象
- 原文地址: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
Why you should give the Closure function another chance
Make your code easier to read with Functional Programming
「譯」一起探討 JavaScript 的對象