關於ES模組你必須要知道的一些禁忌(一)

RIOLI發表於2019-02-16

背景

ES ModuleJavaScriptES2015版本開始提供的語言標準級別的模組化方案,在此之前JavaScript一直沒有語言級別的模組化體系。沒有模組化的支援,使用JavaScript開發大型應用將舉步維艱,所以經過大量的實踐,社群制定了一些模組載入方案,最主要的有執行於瀏覽器的AMD方案和執行於以Nodejs為代表的服務端的CommonJS方案。

由於WebpackBabel等打包、轉義工具的出現,開發者已經可以在開發中使用ES ModuleAMD已是明日黃花,使用的人越來越少,不太值得去關注。但CommonJS方案由於Nodejs在前端構建工具和服務端中的普及度,在Nodejs全面支援ES Module、老版本Nodejs消亡之前,我們還是要關注CommonJS方案以及它與ES Module之間的區別,以免搞混、記憶混淆,釀成bug。為了為後面的禁止點做鋪墊,先讓我們來了解或回顧兩個API:Object.preventExtensionsObject.freeze

Object.preventExtensions

Object.preventExtensions()將物件標記為不再可擴充套件,因此它將永遠不會具有超出它被標記為不可擴充套件的屬性。注意,一般來說,不可擴充套件物件的屬性可能仍然可被刪除。嘗試將新屬性新增到不可擴充套件物件將靜默失敗或丟擲TypeError(在strict mode下)
當我們在嚴格模式下,嘗試對不可擴充的物件進行屬性新增時,就會丟擲異常,具體程式碼如下:

"use strict"
var obj = {
    age: 23,
    name: `rioli`,
    city: [`sz`, `jy`]
};
Object.preventExtensions(obj);
obj.province = `GD`;

執行結果:

Uncaught TypeError: Cannot add property `province`, object is not extensible 

Object.freeze

Object.freeze() 方法可以凍結一個物件,凍結指的是不能向這個物件新增新的屬性,不能修改其已有屬性的值,不能刪除已有屬性,以及不能修改該物件已有屬性的可列舉性、可配置性、可寫性。該方法返回被凍結的物件。
當我們在嚴格模式下,嘗試對已凍結的物件進行屬性修改時,就會丟擲異常,具體程式碼如下:

"use strict"
var obj = {
    age: 23,
    name: `rioli`,
    city: [`sz`, `jy`]
};
Object.freeze(obj);
obj.age = 26;

執行結果:

Uncaught TypeError: Cannot assign to read only property `age` of object `#<Object>`

構建不可擴充的凍結物件

我們可以利用Object.preventExtensionsObject.freeze這兩個API來組合構建一個不可擴充的凍結物件,即:不能對物件的頂級屬性物件增、刪、改等操作。

"use strict"
var obj = {
    age: 23,
    name: `rioli`,
    city: [`sz`, `jy`]
};
Object.freeze(obj);
Object.preventExtensions(obj);
// obj.age = 26; 修改頂級屬性將引發報錯
// obj.province = `GD`;  新增頂級屬性將引發報錯
// delete obj.name; 刪除頂級屬性將引發報錯
// 但仍然可以修改物件的子物件的屬性,因為修改物件的子物件的屬性並不會修改子物件的引用,對於引用型別來說等於沒有發生值的改變
obj.city[0] = `zq`;
obj.city.push(`st`);

ES Module禁忌之不可以修改整體匯入模組物件的直接屬性

為什麼會列出這個禁忌,這是因為在CommonJS方案中,你是可以修改整體匯入模組物件的直接屬性的,長期在CommonJSES Module中交叉使用,難免會造成不必要的記憶混淆。但是在 ES Module中,整體匯入的模組物件是一個不可擴充的凍結的常量物件,對其直接屬性的修改和新增將引發報錯。

假定我們正處於CommonJs環境下,例如NodeJS中,我們匯入等個模組,然後嘗試對模組物件的屬性進行修改和新增,具體程式碼如下:

lib.js

exports.time = Date.now();
exports.getCurrrentYear = function () {
    return new Date().getFullYear();
}
exports.people = {
    age: 26,
    name: `rioli`,
    cities: [`jieyang`, `shenzhen`]
};

main.js

const lib = require(`./lib`);
const people = lib.people;

const print = (data) => {
    console.log("===================================");
    console.log(data);
};

print(people);
print(lib);

people.age = 999;
print(people);

people.father = `baba`;
print(people);

lib.people = 23;
print(lib);

lib.people.age = 666;
print(lib);

lib.provices = [`GD`, `FJ`];
print(lib);

執行結果通過,控制檯輸出結果如下:

===================================
{ age: 26, name: `rioli`, cities: [ `jieyang`, `shenzhen` ] }
===================================
{ time: 1545274516494,
  getCurrrentYear: [Function],
  people: { age: 26, name: `rioli`, cities: [ `jieyang`, `shenzhen` ] } }
===================================
{ age: 999, name: `rioli`, cities: [ `jieyang`, `shenzhen` ] }
===================================
{ age: 999,
  name: `rioli`,
  cities: [ `jieyang`, `shenzhen` ],
  father: `baba` }
===================================
{ time: 1545274516494, getCurrrentYear: [Function], people: 23 }
===================================
{ time: 1545274516494, getCurrrentYear: [Function], people: 23 }
===================================
{ time: 1545274516494,
  getCurrrentYear: [Function],
  people: 23,
  provices: [ `GD`, `FJ` ] }

ES Module環境下,整體匯入的模組物件,但我們對其進行修改和新增屬性的時候,其表現和上述利用Object.preventExtensionsObject.freeze這兩個API來組合構建一個不可擴充的凍結物件的表現一樣,即:這個模組物件是一個不可擴充的凍結的常量物件。演示程式碼如下:
lib.mjs

export const time = Date.now();

export function getCurrrentYear() {
    return new Date().getFullYear();
}

export const people = {
    age: 26,
    name: `rioli`,
    cities: [`jieyang`, `shenzhen`]
};

main.mjs

import { people } from `./lib.mjs`;
import * as lib from `./lib.mjs`;

// 首先定一個基調,其實但我們用 import * as xx from `yy` 將一個模組做整體匯入時,此時xx會作為作為模組名稱空間

const print = (data) => {
    console.log("===================================");
    console.log(data);
};

print(people);
print(lib);

// 允許修改
people.age = 999;
print(people);

// 允許修改
people.father = `baba`;
print(people);

// 不允許修改,因為作為整體匯入時,模組物件的直接一級屬性是隻讀屬性,修改將引發報錯
try {
    lib.people = 23;
    print(lib);
} catch (e) {
    print(e);
    print("不允許修改lib.people,因為作為整體匯入時,模組物件的直接一級屬性是隻讀屬性,修改將引發報錯");
}

// 允許修改,因為作為整體匯入時,模組物件的直接一級屬性是隻讀屬性,但一級
// 屬性引用的物件不是隻讀物件,可以修改其屬性(只要沒有被設定為不可寫)
lib.people.age = 666;
print(lib);

// 整體匯入時,不允許給模組物件新增屬性,因為整體匯入時的模組物件是一個不可擴充的物件,不可以給模組物件新增任何屬性
try {
    lib.provices = [`GD`, `FJ`];
    print(lib);
} catch (e) {
    print(e);
    print("不允許對lib物件新增屬性,整體匯入時,不允許給模組物件新增屬性,因為整體匯入時的模組物件是一個不可擴充的物件,不可以給模組物件新增任何屬性");
}

具體執行結果:

===================================
{ age: 26, name: `rioli`, cities: [ `jieyang`, `shenzhen` ] }
===================================
{ getCurrrentYear: [Function: getCurrrentYear],
  people: { age: 26, name: `rioli`, cities: [ `jieyang`, `shenzhen` ] },
  time: 1545275257126 }
===================================
{ age: 999, name: `rioli`, cities: [ `jieyang`, `shenzhen` ] }
===================================
{ age: 999,
  name: `rioli`,
  cities: [ `jieyang`, `shenzhen` ],
  father: `baba` }
===================================
TypeError: Cannot assign to read only property `people` of object `[object Module]`
    at ModuleJob.run (internal/modules/esm/ModuleJob.js:106:14)
    at <anonymous>
===================================
不允許修改lib.people,因為作為整體匯入時,模組物件的直接一級屬性是隻讀屬性,修改將引發報錯
===================================
{ getCurrrentYear: [Function: getCurrrentYear],
  people:
   { age: 666,
     name: `rioli`,
     cities: [ `jieyang`, `shenzhen` ],
     father: `baba` },
  time: 1545275257126 }
===================================
TypeError: Cannot add property provices, object is not extensible
    at ModuleJob.run (internal/modules/esm/ModuleJob.js:106:14)
    at <anonymous>
===================================
不允許對lib物件新增屬性,整體匯入時,不允許給模組物件新增屬性,因為整體匯入時的模組物件是一個不可擴充的物件,不可以給模組物件新增任何屬性

相關文章