# 物件、陣列與型別判斷

# 物件的擴充修改與調整

下面三種方法是針對物件本身做操作,但因物件有參考特性,無法對巢狀的屬性 有所動作。

# preventExtensions

中文語意:防止擴充

Object.preventExtensions(person);
// 驗證是否可被擴充
console.log('是否可被擴充', Object.isExtensible(person));
console.log(
  'person a 的屬性特徵',
  Object.getOwnPropertyDescriptor(person, 'a')
);

// 調整屬性
person.a = 'a';
// 新增屬性
person.d = 'd';
// 巢狀屬性調整
person.c.a = 'ca';
// 調整特徵
Object.defineProperty(person, 'a', {
  configurable: false,
});
// 刪除
delete person.b;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

輸出結果

# seal

中文語意:封裝

  • 物件屬性無法新增刪除,也無法重新配置特徵,但可以調整目前屬性值
  • 預設物件會被加上 preventExtensions
Object.seal(person);
// 驗證是否可被擴充
console.log('是否可被擴充', Object.isExtensible(person));
// 驗證是否被封裝
console.log('是否被封裝', Object.isSeal(person));
console.log(
  'person a 的屬性特徵',
  Object.getOwnPropertyDescriptor(person, 'a')
);

// 調整屬性
person.a = 'a';
// 新增屬性
person.d = 'd';
// 巢狀屬性調整
person.c.a = 'ca';
// 調整特徵
Object.defineProperty(person, 'a', {
  writable: false,
});
// 刪除
delete person.b;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

輸出結果

# freeze

中文語意:凍結

物件會加上 seal,並且無法調整值。

Object.freeze(person);
// 驗證是否可被擴充
console.log('是否可被擴充', Object.isExtensible(person));
// 驗證是否被封裝
console.log('是否被封裝', Object.isSeal(person));
// 驗證是否被凍結
console.log('是否被凍結', Object.isFrozen(person));
console.log(
  'person a 的屬性特徵',
  Object.getOwnPropertyDescriptor(person, 'a')
);

// 調整屬性
person.a = 'a';
// 新增屬性
person.d = 'd';
// 巢狀屬性調整
person.c.a = 'ca';
// 調整特徵
Object.defineProperty(person, 'a', {
  configurable: false,
});
// 刪除
delete person.b;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

輸出結果

# 理解 JavaScript 建構式

由於函式也是個物件,所以可以借用來當作「建構式」來建立其他物件:

function Person(name, age, gender) {
  this.name = name;
  this.age = age;
  this.gender = gender;

  this.greeting = function() {
    console.log('Hello! My name is ' + this.name + '.');
  };
}

const alex = new Person('Alex', 32, 'male');
alex.greeting(); // "Hello! My name is Alex."

const john = new Person('John', 10, 'male');
john.greeting(); // "Hello! My name is John."
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

像這樣,建立了一個 Person 建構式 (Constructor) ,然後可以就透過 new 關鍵字來建立各種對應的物件。

# 拆解透過 new 建立物件的流程

const alex = new Person('Alex', 32, 'male');

/*
===> const alex = {};
===> Person.call(alex, 'Alex', 32, 'male');
*/
1
2
3
4
5
6

透過 new Person(...) 這個動作,傳回的物件會有 nameagegender 以及 greeting 屬性,而 JavaScript 會在背景執行 Person.call 方法,將 alex 作為 this 的參考物件,然後把這些屬性通通新增到 alex 物件中。

提醒

即使透過建構式建立的物件,這個物件的屬性仍然可以透過 . 來公開存取:

alex.age = 18;
1

# 物件屬性描述器

我們可以透過新的物件模型來控制物件屬性的存取、刪除、列舉等功能。這些特殊的屬性會將它們稱為 屬性描述器 (Property descriptor)

屬性描述器一共可以分為六種:

  • value - 屬性的值
  • writable - 屬性是否可以改變,如果是 false 那就是唯讀屬性
  • enumerable - 物件內的屬性是否可以透過 for-in 語法來迭代
  • configurable - 屬性是否可以被刪除、或修改屬性內的 writableenumerableconfigurable 設定
  • get - 物件屬性的 getter function
  • set - 物件屬性的 setter function

上述除了 value 之外的值都可以不設定,writableenumerableconfigurable 的預設值是 true,而 getset 如果沒有特別指定則是 undefined

這些屬性描述器必須透過 ES5 所提供的 Object.defineProperty() 來處理

# Object.defineProperty 和 Object.getOwnPropertyDescriptor

我們可以透過 Object.defineProperty 來定義物件的屬性描述。

用法:Object.defineProperty(物件對象, 屬性, 屬性描述器)

一般要建立一個簡單物件,可以用:

const person = {
  name: 'alex',
};
1
2
3

另外,也可以透過 Object.defineProperty 來定義物件的屬性:

const person = {};

Object.defineProperty(person, 'name', {
  value: 'alex',
});
1
2
3
4
5

接著可以用 Object.getOwnPropertyDescriptor() 來檢查物件屬性描述器的狀態:

Object.getOwnPropertyDescriptor(person, 'name');

// {
//   configurable: false
//   enumerable: false
//   value: "alex"
//   writable: false
// }
1
2
3
4
5
6
7
8

注意

透過 Object.defineProperty 定義物件屬性,預設的情況下,writableenumerableconfigurable 都是 false

而透過物件實字方式建立的屬性,預設值則會是 true

# 屬性的 getset 方法

使用屬性的 getset 有兩種方法。

  • 利用 物件實字建立物件 的時候,即定義好 getset 的方法

    const wallet = {
      total: 100,
      get save() {
        return this.total / 2;
      },
      set save(price) {
        return (this.total += price / 2);
      },
    };
    
    console.log(wallet.save, wallet);
    // 使用等號賦值,而非函式
    wallet.save = 300;
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    預設 save 屬性為 (...),當點開後,才會針對目前的 totalgetter 取值 輸出結果

  • 利用 Object.defineProperty() 定義

    const wallet = {
      total: 100;
    }
    // 預設的 save 屬性,configurable 和 enumerable 皆為 false
    Object.defineProperty(wallet, 'save', {
      // 可以選擇是否加回來
      // configurable: true,
      // enumerable: true,
      get() {
        return this.total / 2;
      },
      set(price) {
        return this.total += price / 2
      }
    })
    
    wallet.save = 300;
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17

    輸出結果

注意

如果定義了 getset 的方法,表示要自行控制屬性的存取範圍,那麼就不能再去定義 valuewritable 的屬性描述

Object.defineProperty(person, 'firstName', {
  value: 'firstName',
  writable: true,
  get: function() {
    return this.name;
  },
  set: function(name) {
    this.name = name;
  },
});

// Uncaught TypeError: Invalid property descriptor.
// Cannot both specify accessors and a value or writable attribute, #<Object>
1
2
3
4
5
6
7
8
9
10
11
12
13

使用 get 方法新增 Array 的原型方法:

const a = [1, 2, 3];
// 直接操作陣列原型
Object.defineProperty(Array.prototype, 'latest', {
  get() {
    return this[this.length - 1];
  },
});

a.latest; // 3
1
2
3
4
5
6
7
8
9

# 陣列

陣列的長度可以由 array.length 來取得,而 length 屬性的值是 可以被覆寫 的:

const animals = ['cat', 'dog', 'bird'];

animals.length = 1;
console.log(animals); // ['cat']

animals.length = 3;
console.log(animals); // ['cat', undefined, undefined]
1
2
3
4
5
6
7

上面的例子中,陣列 animals 原本的長度為 3,後來透過 animals.length = 1; 設定成 1 之後,後面的元素就被移除了。 即使之後再度修改成 animals.length = 3;,後面的兩個元素也只會被 undefined 所填補。

# 如何判斷是否為陣列

可以使用 isArray() 方法:

Array.isArray([]); // true
Array.isArray([1]); // true
Array.isArray(new Array()); // true
1
2
3

# 型別判斷

若要在 JavaScript 中檢查變數型別,正確來說應該是 值的型別,變數沒有型別,值才有,可以透過 typeof 運算子來處理。

要注意的是,透過 typeof 運算子回傳的東西都是 字串

# 為什麼函式的型別是 function 而不是 object

當我們透過 typeof 去檢查一個「函式 (function) 」的時候,雖然會得到 "function" 的結果,誤以為 function 也是 JavaScript 定義的一種型別,但實際上它仍屬於 Object 的一種,可以把它想像成是一種可以被呼叫 (be invoked) 的特殊物件。

MDN 對 function 的定義:

Every JavaScript function is actually a Function object.

This can be seen with the code (function(){}).constructor === Function, which returns true.

# Pass by sharing

當函式的參數被 整個 重新賦值的時候,外部變數的內容是不會被影響的。

const coin = { value: 10 };

const changeValue = (coin) => {
  return (coin = { value: 123 });
};

changeValue(coin); // {value: 123}
console.log(coin); // {value: 10}
1
2
3
4
5
6
7
8

但如果是 屬性 被重新賦值,則會發生改變:

const changeValue = (coin) => {
  return (coin.value = 123);
};

changeValue(coin); // 123
console.log(coin); // {value: 123}
1
2
3
4
5
6

# 參考

深入理解 JavaScript 物件屬性 (opens new window)

Last Updated: 2020/11/12 上午4:23:18