編寫高質量JavaScript代碼之對象和原型

參考書本:《Effective JavaScript》

對象和原型

明白prototype、getPrototypeOf和__proto__之間的差別

原型包括三個自力但相干的接見器。

  • C.prototype用於豎立由new C()建立的對象的原型。
  • Object.getPrototypeOf(obj)是ES5中用來獵取obj對象的原型對象的範例要領。
  • obj.__proto__是獵取obj對象的原型對象的非範例要領。
function User(name, passwordHash) {
    this.name = name;
    this.passwordHash = passwordHash;
}

User.prototype.toString = function () {
    return '[User ' + this.name + ']';
};

User.prototype.checkPassword = function (password) {
    return hash(password) === this.passwordHash;
}

var u = new User('sfalken', '0ef33ae791068ec64b502d6cb0191387');

User函數帶有一個默許的prototype屬性,其包括一個最先險些為空的對象。當我們運用new操縱符建立User的實例時,發生的對象u得到了自動分派的原型對象,該原型對象被存儲在User.prototype中。

Object.getPrototypeOf(u) === User.prototype; // true
u.__proto__ === User.prototype; // true

提醒:

  • C.prototype屬性是new C()建立的對象的原型。
  • Object.getPrototypeOf(obj)是ES5中檢索對象原型的範例函數。
  • Obj.__proto__是檢索對象原型的非範例函數。
  • 類是由一個組織函數和一個關聯的原型構成的一種設想形式。

運用Object.getPrototypeOf函數而不要運用__proto__屬性

__proto__屬性供應了Object.getPrototypeOf要領所不具備的分外才能,即修正對象原型鏈接的才能。這類才能會形成嚴峻的影響,應該防止運用,緣由以下:

  1. 可移植性:並非一切的平台都支撐轉變對象原型的特徵,所以沒法編寫可移植的代碼。
  2. 機能題目:當代的JavaScript引擎痘深度優化了獵取和設置對象屬性的行動,如更改了對象的內部構造(如增添或刪除該對象或其原型鏈中的對象的屬性)會使一些優化失效。
  3. 可展望性:修正對象的原型鏈會影響對象的全部繼續條理構造,在某些情況下如許的操縱能夠有用,然則堅持繼續條理構造的相對穩定是一個基礎的原則。

能夠運用ES5中的Object.create函數來建立一個具有自定義原型鏈的新對象。

提醒:

  • 一直不要修正對象的__proto__屬性。
  • 運用Object.create函數給新對象設置自定義的原型。

使組織函數與new操縱符無關

function User(name, passwordHash) {
    this.name = name;
    this.passwordHash = passwordHash;
}

var u = User('baravelli', 'd8b74df393528d51cd19980ae0aa028e');
u; // undefined
this.name; // baravelli
this.passwordHash; // d8b74df393528d51cd19980ae0aa028e

假如挪用者遺忘運用new關鍵字,該函數不但會返回無意義的undefined,而且會建立(假如這些全局變量已存在則會修正)全局變量name和passwordHash。

假如將User函數定義為ES5的嚴厲代碼,那末它的接收者默許為undefined。

function User(name, passwordHash) {
    "use strict";

    this.name = name;
    this.passwordHash = passwordHash;
}

var u = User('baravelli', 'd8b74df393528d51cd19980ae0aa028e'); // Uncaught TypeError: Cannot set property 'name' of undefined

一個更加硬朗的體式格局是供應一個不管怎麼挪用都事情如組織函數的函數。

function User(name, passwordHash) {
    if (!this instanceof User) {
        return new User(name, passwordHash);
    }

    this.name = name;
    this.passwordHash = passwordHash;
}

var x = User('baravelli', 'd8b74df393528d51cd19980ae0aa028e'); 
var y = new User('baravelli', 'd8b74df393528d51cd19980ae0aa028e'); 
x instanceof User; // true
y instanceof User; // true

上述形式的一個瑕玷是它須要分外的函數挪用,且難適用於可變參數函數,因為沒有一種模仿apply要領將可變參數函數作為組織函數挪用的體式格局。

一種更加奇特的體式格局是應用ES5的Object.create函數。

function User(name, passwordHash) {
    var self = this instanceof User ? this : Object.create(User.prototype);

    self.name = name;
    self.passwordHash = passwordHash;

    return self;
}

Object.create須要一個原型對象作為參數,並返回一個繼續自原型對象的新對象。

多虧了組織函數掩蓋形式,運用new操縱符挪用上述User函數的行動與以函數挪用它的行動是一樣的,這能事情完整得益於JavaScript許可new表達式的效果能夠被組織函數的顯現return語句所掩蓋

提醒:

  • 經由過程運用new操縱符或Object.create要領在組織函數定義中挪用本身使得該組織函數與挪用語法無關。
  • 當一個函數希冀運用new操縱符挪用時,清楚地文檔化該函數。

在原型中存儲要領

JavaScript完整有能夠不藉助原型舉行編程。

function User(name, passwordHash) {
    this.name = name;
    this.passwordHash = passwordHash;
    this.toString = function () {
        return 'User ' + this.name + ']';
    };
    this.checkPassword = function (password) {
        return hash(password) === this.passwordHash;
    }
} 

var u1 = new User(/* ... */);
var u2 = new User(/* ... */);
var u3 = new User(/* ... */);

上述代碼中的每一個實例都包括toString和checkPassword要領的副本,而不是經由過程原型同享這些要領。

將要領存儲在原型,使其能夠被一切的實例運用,而不須要存儲要領完成的多個副本,也不須要給每一個實例對象增添分外的屬性。

同時,當代的JavaScript引擎深度優化了原型查找,所以將要領複製到實例對象並不一定保證查找的速率有顯著的提拔,而且實例要領比起原型要領肯定會佔用更多的內存。

提醒:

  • 將要領存儲在實例對象中會建立該函數的多個副本,因為每一個實例對象都有一份副本。
  • 將要領存儲於原型中優於存儲在實例對象中。

運用閉包存儲私有數據

恣意一段順序都能夠簡樸地經由過程接見JavaScript對象的屬性名來獵取響應地對象屬性,比方for in輪迴、ES5的Object.keys函數和Object.getOwnPropertyNames函數。

一些順序員運用定名範例給私有屬性前置或後置一個下劃線字符_。

但是實際上,一些順序須要更高水平的信息隱蔽。

關於這類情況,JavaScript為信息隱蔽供應了閉包。閉包將數據存儲到關閉的變量中而不供應對這些變量的直接接見,獵取閉包內部構造的唯一體式格局是該函數顯式地供應獵取它的門路。

應用這一特徵在對象中存儲真正的私有數據。不是將數據作為對象的屬性來存儲,而是在組織函數中以變量的體式格局存儲它。

function User(name, passwordHash) {
    this.toString = function () {
        return '[User ' + name + ']';
    };
    this.checkPassword = function (password) {
        return hash(password) === passwordHash;
    } 
}

上述代碼的toString和checkPassword要領是以變量的體式格局來援用name和passwordHash變量的,而不是以this屬性的體式格局來援用,User的實例不包括任何實例屬性,因而外部的代碼不能直接接見User實例的name和passwordHash變量。

該形式的一個瑕玷是,為了讓組織函數中的變量在運用它們的要領的作用域內,這些要領必需放置於實例對象中,這會致使要領副本的散布。

提醒:

  • 閉包變量是私有的,只能經由過程部分的援用獵取。
  • 將部分變量作為私有數據從而經由過程要領完成信息隱蔽。

只將實例狀況存儲在實例對象中

一種毛病的做法是不小心將每一個實例的數據存儲到了其原型中。

function Tree(x) {
    this.value = x;
}

Tree.prototype = {
    children: [], // should be instance state!
    addChild: function(x) {
        this.children.push(x);
    }
};

var left = new Tree(2);
left.addChild(1);
left.addChild(3);

var right = new Tree(6);
right.addChild(5);
right.addChild(7);

var top = new Tree(4);
top.addChild(left);
top.addChild(right);

top.children; // [1, 3, 5, 7, left, right]

每次挪用addChild要領,都會將值增添到Tree.prototype.children數組中。

完成Tree類的準確體式格局是為每一個實例對象建立一個零丁的children數組。

function Tree(x) {
    this.value = x;
    this.children = []; // instance state
}

Tree.prototype = {
    addChild: function(x) {
        this.children.push(x);
    }
};

平常情況下,任何不可變的數據能夠被存儲在原型中從而被安全地同享。有狀況的數據原則上也能夠存儲在原型中,只需你真正想同享它。但是迄今為止,在原型對象中最罕見的數據是要領,而每一個實例的狀況都存儲在實例對象中。

提醒:

  • 同享可變數據能夠會出題目,因為原型是被其一切的實例同享的。
  • 將可變的實例狀況存儲在實例對象中。

認識到this變量的隱式綁定題目

編寫一個簡樸的、可定製的讀取CSV(逗號分開型取值)數據的類。

function CSVReader(separators) {
    this.separators = separators || [','];
    this.regexp = new RegExp(this.separators.map(function (sep) {
        return '\\' + sep[0];
    }).join('|'));
}

完成一個簡樸的read要領能夠分為兩步來處置懲罰。第一步,將輸入的字符串分為按行分別的數組。第二步,將數組的每一行再分為按單元格分別的數組。效果取得一個二維的字符串數組。

CSVReader.prototype.read = function (str) {
    var lines = str.trim().split(/\n/);
    return lines.map(function (line) {
        return line.split(this.regexp);
    });
};

var reader = new CSVReader();
reader.read('a, b, c\nd, e, f\n'); // [['a, b, c'], ['d, e, f']]

上述代碼的bug是,傳遞給line.map的回調函數援用的this指向的是window,因而,this.regexp發生undefined值。

備註:'a, b, c'.split(undefined)返回['a, b, c']

  1. 榮幸的是,數組的map要領能夠傳入一個可選的參數作為其回調函數的this綁定。

    CSVReader.prototype.read = function (str) {
        var lines = str.trim().split(/\n/);
        return lines.map(function (line) {
            return line.split(this.regexp);
        }, this);
    };
    
    var reader = new CSVReader();
    reader.read('a, b, c\nd, e, f\n'); // [['a', 'b', 'c'], ['d', 'e', 'f']]
  2. 然則,不是一切基於回調函數的API都斟酌全面。另一種解決方案是運用詞法作用域的變量來存儲這個分外的外部this綁定的援用。

    CSVReader.prototype.read = function (str) {
        var lines = str.trim().split(/\n/);
        var self = this; // save a reference to outer this-binding
        return lines.map(function (line) {
            return line.split(this.regexp);
        });
    };
    
    var reader = new CSVReader();
    reader.read('a, b, c\nd, e, f\n'); // [['a', 'b', 'c'], ['d', 'e', 'f']]
  3. 在ES5的環境中,另一種有用的要領是運用回調函數的bind要領。

    CSVReader.prototype.read = function (str) {
        var lines = str.trim().split(/\n/);
        return lines.map(function (line) {
            return line.split(this.regexp);
        }.bind(this)); // bind to outer this-binding
    };
    
    var reader = new CSVReader();
    reader.read('a, b, c\nd, e, f\n'); // [['a', 'b', 'c'], ['d', 'e', 'f']]

提醒:

  • this變量的作用域老是由其近來的關閉函數所肯定。
  • 運用一個部分變量(一般定名為self、me或that)使得this綁定關於內部函數是可用的。

在子類的組織函數中挪用父類的組織函數

場景圖(scene graph)是在可視化的順序中(如遊戲或圖形仿真場景)形貌一個場景的對象鳩合。一個簡樸的場景包括了在該場景中的一切對象(稱為角色),以及一切角色的預加載圖象數據集,還包括一個底層圖形顯現的援用(一般被稱為context)。

function Scene(context, width, height, images) {
    this.context = context;
    this.width = width;
    this.height = height;
    this.images = images;
    this.actors = [];
}

Scene.prototype.register = function (actor) {
    this.actors.push(actor);
};

Scene.prototype.unregister = function (actor) {
    var i = this.actors.indexOf(actor);
    
    if (i >= 0) {
        this.actors.splice(i, 1);
    }
};

Scene.prototype.draw = function () {
    this.context.clearRect(0, 0, this.width, this.height);
    
    for (var a = this.actors, i = 0, n = a.length; i < n; i++) {
        a[i].draw();
    }
};

場景中的一切角色都繼續自基類Actor。

function Actor(scene, x, y) {
    this.scene = scene;
    this.x = x;
    this.y = y;
    scene.register(this);
}

Actor.prototype.moveTo = function (x, y) {
    this.x = x;
    this.y = y;
    this.scene.draw();
};

Actor.prototype.exit = function() {
    this.scene.unregister(this);
    this.scene.draw();
};

Actor.prototype.draw = function () {
    var image = this.scene.images[this.type];
    this.scene.context.drawImage(image, this.x, this.y);
};

Actor.prototype.width = function () {
    return this.scene.images[this.type].width;
};

Actor.prototype.height = function () {
    return this.scene.images[this.type].height;
};

我們將角色的特定範例完成為Actor的子類。比方,在街機遊戲中太空飛船就會有一個拓展自Actor的SpaceShip類。

為了確保SpaceShip的實例能作為角色被準確地初始化,其組織函數必需顯式地挪用Actor的組織函數。經由過程將接收者綁定到該新對象來挪用Actor能夠達到此目標。

function SpaceShip(scene, x, y) {
    Actor.call(this, scene, x, y);
    this.points = 0;
}

挪用Actor的組織函數能確保Actor建立的一切實例屬性都被增添到了新對象(SpaceShip實例對象)中。為了使SpaceShip成為Actor的一個準確地子類,其原型必需繼續自Actor.prototype。做這類拓展的最好的體式格局是運用ES5供應的Object.create要領。

SpaceShip.prototype = Object.create(Actor.prototype);

一旦建立了SpaceShip的原型對象,我們就能夠向其增添一切的可被實例同享的屬性。

SpaceShip.prototype.type = 'spaceShip';

SpaceShip.prototype.scorePoint = function () {
    this.points++;
};

SpaceShip.prototype.left = function () {
    this.moveTo(Math.max(this.x - 10, 0), this.y);
};

SpaceShip.prototype.right = function () {
    var maxWidth = this.scene.width - this.width();
    this.moveTo(Math.min(this.x + 10, maxWidth), this.y);
};

提醒:

  • 在子類組織函數中顯現地傳入this作為顯式地接收者挪用父類組織函數。
  • 運用Object.create函數來組織子類的原型對象以防止挪用父類的組織函數。

不要重用父類的屬性名

function Actor(scene, x, y) {
    this.scene = scene;
    this.x = x;
    this.y = y;
    this.id = ++Actor.nextID;
    scene.register(this);
}

Actor.nextID = 0;
function Alien(scene, x, y, direction, speed, strength) {
    Actor.call(this, scene, x, y);
    this.direction = direction;
    this.speed = speed;
    this.strength = strength;
    this.damage = 0;
    this.id = ++Alien.nextID; // conflicts with actor id!
}

Alien.nextID = 0;

Alien類與其父類Actor類都視圖給實例屬性id寫數據。假如在繼續體系中的兩個類指向雷同的屬性名,那末它們指向的是同一個屬性。

該例子不言而喻的解決要領是對Actor標識數和Alien標識數運用差別的屬性名。

function Actor(scene, x, y) {
    this.scene = scene;
    this.x = x;
    this.y = y;
    this.actorID = ++Actor.nextID; // distinct from alienID
    scene.register(this);
}

Actor.nextID = 0;

function Alien(scene, x, y, direction, speed, strength) {
    Actor.call(this, scene, x, y);
    this.direction = direction;
    this.speed = speed;
    this.strength = strength;
    this.damage = 0;
    this.alienID = ++Alien.nextID; // distinct from actorID
}

Alien.nextID = 0;

提醒:

  • 注意父類運用的一切屬性名。
  • 不要在子類中重用父類的屬性名。

防止繼續範例類

一個操縱文件體系的庫能夠願望建立一個籠統的目次,該目次繼續了數組的一切行動。

function Dir(path, entries) {
    this.path = path;
    for (var i = 0, n = entries.length; i < n; i++) {
        this[i] = entries[i];
    }
}

Dir.prototype = Object.create(Array.prototype); // extends Array

遺憾的是,這類體式格局損壞了數組的length屬性的預期行動。

var dir = new Dir('/tmp/mysite', ['index.html', 'script.js', 'style.css']);

dir.length; // 0

失利的緣由是length屬性只對在內部標記為“真正的”數組的迥殊對象起作用。ECMAScript範例劃定它是一個不可見的內部屬性,稱為[[Class]]。

數組對象(經由過程Array組織函數或[]語法建立)被加上了值為“Array”的[[Class]]屬性,函數被加上了值為“Function”的[[Class]]屬性。

事實證明,length的行動只被定義在內部屬性[[Class]]的值為“Array”的迥殊對象中。關於這些對象,JavaScript堅持length屬性與該對象的索引屬性的數目同步。

但當我們拓展Array類時,子類的實例並非經由過程new Array()或字面量[]語法建立的。所以,Dir的實例[[Class]]屬性值為“Object”。

更好的完成是定義一個entries數組的實例屬性。

function Dir(path, entries) {
    this.path = path;
    this.entries = entries; // array property
}

Dir.prototype.forEach = function (f, thisArg) {
    if (typeof thisArg === 'undefined') {
        thisArg = this;
    }

    this.entries.forEach(f, thisArg);
};

提醒:

  • 繼續範例類每每因為一些迥殊的內部屬性(如[[Class]])而被損壞。
  • 運用屬性託付優於繼續範例類。

將原型視為完成細節

原型是一種對象行動的完成細節。

JavaScript供應了方便的內省機制(introspection mechanisms)來搜檢對象的細節。Object.prototype.hasOwnProperty要領肯定一個屬性是不是為對象“本身的”屬性(即一個實例屬性),而完整疏忽原型繼續機構。Object.getPrototypeOf__proto__特徵許可順序員遍歷對象的原型鏈並零丁查詢其原型對象。

搜檢完成細節(縱然沒有修正它們)也會在順序的組件之間建立依靠。假如對象的生產者修正了完成細節,那末依靠於這些對象的運用者就會被損壞。

提醒:

  • 對象是接口,原型是完成。
  • 防止搜檢你沒法控制的對象的原型構造。
  • 防止搜檢完成在你沒法控制的對象內部的屬性。

防止運用草率的猴子補丁

因為對象同享原型,因而每一個對象都能夠增添、刪除或修正原型的屬性,這個有爭議的實踐一般被稱為猴子補丁(monkey-patching)。

猴子補丁的吸引力在於它的壯大,數組缺乏一個有用的要領,你本身就能夠增添它。

Array.prototype.split = function (i) { // alternative #1
    return [this.slice(0, 1), this.slice(i)];
};

然則當多個庫以不兼容的體式格局給同一個原型打猴子補丁時,題目就湧現了。

Array.prototype.split = function (i) { // alternative #2
    var i = Math.floor(this.length / 2);
    return [this.slice(0, 1), this.slice(i)];
};

如今,任一對數組split要領的運用都大約有50%的機會被損壞。

一個要領能夠將這些修正置於一個函數中,用戶能夠挑選挪用或疏忽。

function addArrayMethods() {
    Array.prototype.split = function (i) {
        return [this.slice(0, 1), this.slice(i)];
    }
}

只管猴子補丁很風險,然則有一種迥殊牢靠而且有價值的運用場景:polyfill。

if (typeof Array.prototype.map !== 'function') {
    Array.prototype.map = function (f, thisArg) {
        var result = [];
        
        for (var i = 0, n = this.length; i < n; i++) {
            result[i] = f.call(thisArg, this[i], i);
        }

        return result;
    };
}

提醒:

  • 防止運用草率的猴子補丁。
  • 紀錄順序庫所實行的一切猴子補丁。
  • 斟酌經由過程將修正置於一個隨處函數中,使猴子補丁稱為可選的。
  • 運用猴子補丁為缺失的範例API供應polyfills。
    原文作者:3santiago3
    原文地址: https://segmentfault.com/a/1190000014668472
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞