1. 程式人生 > >「譯」一起探討 JavaScript 的對象

「譯」一起探討 JavaScript 的對象

apply() ati 讀取 number 隱藏屬性 公有 cover users global

「譯」一起探討 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引擎對創建包裝和銷毀包裝對象的過程做了優化。

數值、字符串和布爾值都有等效的包裝對象。跟別是:NumberStringBoolean

nullundefined 原始值沒有相應的包裝對象並且不提供任何方法。

內置原型

Numbers 繼承自Number.prototypeNumber.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() 等方法。

所有對象、函數和原始值(除了 nullundefined )都從 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 的對象