「譯」一塊兒探討 JavaScript 的對象


一塊兒探討 JavaScript 的對象

對象是多個屬性的動態集合,它有一個連接着原型的隱藏屬性(注:__proto__)。javascript

一個屬性擁有一個 key 和一個 value 。java

屬性的 key

屬性的 key 是一個惟一的字符串。git

訪問屬性有兩種方式:點表示法和括號表示法。當使用點表示法,屬性的 key 必須是有效的標識符。github

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()方法(若是可用的話)把它轉換爲字符串。bash

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 。閉包

屬性的值

屬性的值能夠是任意的基礎數據類型,對象,或函數。app

對象做爲值

對象能夠嵌套在其餘對象裏。看下面這個例子函數

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

相關文章
相關標籤/搜索