如何禁止JavaScript物件重寫?

Fundebug發表於2019-02-27

譯者按: 使用Object.preventExtensions()、Object.seal()和Object.freeze(),可以禁止重寫JavaScript物件。

由於JavaScript的靈活性,我們可以輕易地重寫(override)一些於其他人定義的物件(object)。換句話說,任何人都可以重寫我們所定義的物件。這是一個非常強大的特性,許多開發者都有興趣試試,來擴充或者修改某些物件的行為。例如,DOM方法document.getElementById()都可以被重寫。一般來講,我們應該避免這樣做,因為這會導致程式碼很難維護,並且會留下一些難於發現的BUG。ECMAScript 5引入了一些方法,允許開發者限制物件重寫。如果你在開發一些工具庫比如jQuery, fundebug等, 或者你的開發團隊非常大,本文介紹的這些方法將非常有用。

不要重寫他人的物件

不要重寫他人的物件,這是JavaScript的黃金法則。比如,當你重寫了一個方法,則很可能這會影響依賴於該方法的庫,這會讓其他開發者非常困惑。

// 示例程式碼1
window.originalAlert = window.alert;  
window.alert = function(msg) {  
    if (typeof msg === "string") {
        return console.log(msg);
    }
    return window.originalAlert(msg);
};

alert(`ooh so awesome`); // 引數為字串時,列印到控制檯 
alert(3.14); // 引數為其他型別時,彈出對話方塊
複製程式碼

示例程式碼1中,我修改了windows.alert:引數為字串時,列印到控制檯;引數為其他型別時,彈出對話方塊。這樣的修改顯然會影響其他使用alert方法的開發者。如果你修改的是DOM物件比如getElementById(),這會導致非常嚴重的後果。

如果你只是為物件新增新的方法,這也會導致問題。

// 示例程式碼2
Math.cube = function(n) {  
    return Math.pow(n, 3);
};
console.log(Math.cube(2)); // 8
複製程式碼

這樣做最大的問題是有可能在未來導致命名衝突。儘管Math物件目前並沒有cube方法,下一個版本的JavaScript標準也許會增加cube方法(當然可能性不大),這就意味著我們會把原生cube方法給替代了。有一個真實的案例,Prototype庫定義了document.getElementsByClassName()方法,而這個方法後來被加入了JavaScript標準。

不幸的是,我們無法阻止其他開發者重寫我們定義的物件,這時我們就需要本文介紹的這些方法了:

首先,我們不妨通過一個表格對比一下Object.preventExtensions()、Object.seal()和Object.freeze():

方法 禁止增加屬性 禁止刪除屬性 禁止修改屬性
Object.preventExtensions()
Object.seal()
Object.freeze()

Object.preventExtensions()

使用Object.preventExtensions(),可以禁止給物件新增新的方法或者屬性。注意,修改或者刪除物件已經存在的方法或者屬性是沒有問題的。使用Object.isExtensible()可以檢視某個物件是否可以增加方法或者屬性。

// 示例程式碼3
var song = {  
    title: `Hope Leaves`,
    artist: `Opeth`
};


console.log(Object.isExtensible(song)); //true  
Object.preventExtensions(song);  
console.log(Object.isExtensible(song)); //false  


song.album = `Damnation`;
console.log(song.album);  // undefined


song.play = function() {  
    console.log(`ahh soo awesome`);
};
song.play(); // TypeError: song.play is not a function
複製程式碼

示例程式碼3可知,執行Object.preventExtensions()之後,為song物件新增album以及play方法都失敗了!

但是,當我們為song新增屬性或者方法時,並沒有報錯。當我們使用了”use strict”採用嚴格模式時,情況就不一樣了:

// 示例程式碼4
"use strict";

var song = {  
    title: `Hope Leaves`,
    artist: `Opeth`
};

Object.preventExtensions(song);  

song.album = `Damnation`; // Uncaught TypeError: Cannot add property album, object is not extensible
複製程式碼

在嚴格模式下,給已經Object.preventExtensions的物件新增屬性時,會立即報錯。廣告:如果你希望實時監控應用中類似的錯誤,歡迎免費試用Fundebug

Object.seal()

使用Object.seal(),可以禁止給物件新增屬性或者方法(這一點與Object.preventExtension()的作用一致),同時禁止刪除物件已經存在的屬性或者方法。

// 示例程式碼5
"use strict"
var song = {
    title: `Hope Leaves`,
    artist: `Opeth`
};

Object.seal(song);
console.log(Object.isExtensible(song)); //false  
console.log(Object.isSealed(song)); //true  

song.album = `Damnation`; // Uncaught TypeError: Cannot add property album, object is not extensible
delete song.artist; // Uncaught TypeError: Cannot delete property `artist` of #<Object>
複製程式碼

Object.freeze()

使用Object.freeze(),可以禁止為物件增加屬性或者方法(這一點與Object.preventExtension()的作用一致),同時禁止刪除物件已經存在的屬性或者方法(這一點與Object.seal()的作用一致),另外還禁止修改已經存在的屬性或者方法。

// 示例程式碼6
"use strict"
var song = {
    title: `Hope Leaves`,
    artist: `Opeth`,
    getLongTitle: function()
    {
        return this.artist + " - " + this.title;
    }
};

Object.freeze(song);

console.log(Object.isExtensible(song)); // false  
console.log(Object.isSealed(song)); // true  
console.log(Object.isFrozen(song)); // true  

song.album = `Damnation`; // Uncaught TypeError: Cannot add property album, object is not extensible  
delete song.artist; // Uncaught TypeError: Cannot delete property `artist` of #<Object> 
song.getLongTitle = function() // Uncaught TypeError: Cannot assign to read only property `getLongTitle` of object `#<Object>`
{
    return "foobar";
};
複製程式碼

主流瀏覽器的最新版本都支援這些方法:

  • IE 9+
  • Firefox 4+
  • Safari 5.1+
  • Chrome 7+
  • Opera 12+

相關文章