1. 程式人生 > >對象的擴展

對象的擴展

default all 沒有 我們 語法 代碼 symbol 獲取 err

本文為學習筆記;

原文鏈接?<<<<轉載請註明原文出處!

對象(object)是 JavaScript 最重要的數據結構。ES6 對它進行了重大升級,本章介紹數據結構本身的改變

屬性的簡潔表示法

ES6 允許直接寫入變量和函數,作為對象的屬性和方法。這樣的書寫更加簡潔。

const foo = 'bar';
const baz = {foo};
baz // {foo: "bar"}

// 等同於
const baz = {foo: foo};

上面代碼表明:ES6 允許在對象之中,直接寫變量。這時,屬性名為變量名, 屬性值為變量的值。

下面是另一個例子。

function f(x, y) {
  return {x, y};
}

// 等同於

function f(x, y) {
  return {x: x, y: y};
}

f(1, 2) // Object {x: 1, y: 2}

除了屬性簡寫,方法也可以簡寫。

const o = {
  method() {
    return "Hello!";
  }
};

// 等同於

const o = {
  method: function() {
    return "Hello!";
  }
};

下面是一個實際的例子。

let birth = '2000/01/01';

const Person = {

  name: '張三',

  //等同於birth: birth
  birth,

  // 等同於hello: function ()...
  hello() { console.log('我的名字是', this.name); }

};

這種寫法用於函數的返回值,將會非常方便。

function getPoint() {
  const x = 1;
  const y = 10;
  return {x, y};
}

getPoint()
// {x:1, y:10}

CommonJS 模塊輸出一組變量,就非常合適使用簡潔寫法。

let ms = {};

function getItem (key) {
  return key in ms ? ms[key] : null;
}

function setItem (key, value) {
  ms[key] = value;
}

function clear () {
  ms = {};
}

module.exports = { getItem, setItem, clear };
// 等同於
module.exports = {
  getItem: getItem,
  setItem: setItem,
  clear: clear
};

屬性的賦值器(setter)和取值器(getter),事實上也是采用這種寫法。

const cart = {
  _wheels: 4,

  get wheels () {
    return this._wheels;
  },

  set wheels (value) {
    if (value < this._wheels) {
      throw new Error('數值太小了!');
    }
    this._wheels = value;
  }
}

註意,簡潔寫法的屬性名總是字符串,這會導致一些看上去比較奇怪的結果。

const obj = {
  class () {}
};

// 等同於

var obj = {
  'class': function() {}
};

上面代碼中,class是字符串,所以不會因為它屬於關鍵字,而導致語法解析報錯。

如果某個方法的值是一個 Generator 函數,前面需要加上星號。

const obj = {
  * m() {
    yield 'hello world';
  }
};

屬性名表達式

JavaScript 定義對象的屬性,有兩種方法。

// 方法一
obj.foo = true;

// 方法二
obj['a' + 'bc'] = 123;

上面代碼的方法一是直接用標識符作為屬性名,方法二是用表達式作為屬性名,這時要將表達式放在方括號之內。

但是,如果使用字面量方式定義對象(使用大括號),在 ES5 中只能使用方法一(標識符)定義屬性。

var obj = {
  foo: true,
  abc: 123
};

ES6 允許字面量定義對象時,用方法二(表達式)作為對象的屬性名,即把表達式放在方括號內。

let propKey = 'foo';

let obj = {
  [propKey]: true,
  ['a' + 'bc']: 123
};

下面是另一個例子。

let lastWord = 'last word';

const a = {
  'first word': 'hello',
  [lastWord]: 'world'
};

a['first word'] // "hello"
a[lastWord] // "world"
a['last word'] // "world"

表達式還可以用於定義方法名。

let obj = {
  ['h' + 'ello']() {
    return 'hi';
  }
};

obj.hello() // hi

註意,屬性名表達式與簡潔表示法,不能同時使用,會報錯。

// 報錯
const foo = 'bar';
const bar = 'abc';
const baz = { [foo] };

// 正確
const foo = 'bar';
const baz = { [foo]: 'abc'};

註意,屬性名表達式如果是一個對象,默認情況下會自動將對象轉為字符串[object Object],這一點要特別小心。

const keyA = {a: 1};
const keyB = {b: 2};

const myObject = {
  [keyA]: 'valueA',
  [keyB]: 'valueB'
};

myObject // Object {[object Object]: "valueB"}

上面代碼中,[keyA]和[keyB]得到的都是[object Object],所以[keyB]會把[keyA]覆蓋掉,而myObject最後只有一個[object Object]屬性。

方法的 name 屬性

  • 函數的name屬性,返回函數名。對象方法也是函數,因此也有name屬性。

    const person = {
      sayName() {
        console.log('hello!');
      },
    };
    
    person.sayName.name   // "sayName"

    上面代碼中,方法的name屬性返回函數名(即方法名)。

  • 如果對象的方法使用了取值函數(getter)和存值函數(setter),則name屬性不是在該方法上面,而是該方法的屬性的描述對象的get和set屬性上面,返回值是方法名前加上get和set。

    ```js
    const obj = {
    get foo() {},
    set foo(x) {}
    };

    obj.foo.name
    // TypeError: Cannot read property ‘name‘ of undefined

    const descriptor = Object.getOwnPropertyDescriptor(obj, ‘foo‘);

    descriptor.get.name // "get foo"
    descriptor.set.name // "set foo"

  • 有兩種特殊情況:bind方法創造的函數,name屬性返回bound加上原函數的名字;Function構造函數創造的函數,name屬性返回anonymous。

    (new Function()).name // "anonymous"
    
    var doSomething = function() {
      // ...
    };
    doSomething.bind().name // "bound doSomething"
  • 如果對象的方法是一個 Symbol 值,那麽name屬性返回的是這個 Symbol 值的描述。

    const key1 = Symbol('description');
    const key2 = Symbol();
    let obj = {
      [key1]() {},
      [key2]() {},
    };
    obj[key1].name // "[description]"
    obj[key2].name // ""

    上面代碼中,key1對應的 Symbol 值有描述,key2沒有。

屬性的可枚舉性和遍歷


可枚舉性

對象的每個屬性都有一個描述對象(Descriptor),用來控制該屬性的行為。Object.getOwnPropertyDescriptor方法可以獲取該屬性的描述對象。

let obj = { foo: 123 };
Object.getOwnPropertyDescriptor(obj, 'foo')
//  {
//    value: 123,
//    writable: true,
//    enumerable: true,
//    configurable: true
//  }

描述對象的enumerable屬性,稱為”可枚舉性“,如果該屬性為false,就表示某些操作會忽略當前屬性。

目前,有四個操作會忽略enumerable為false的屬性:

  • for...in循環:只遍歷對象自身的和繼承的可枚舉的屬性。
  • Object.keys():返回對象自身的所有可枚舉的屬性的鍵名。
  • JSON.stringify():只串行化對象自身的可枚舉的屬性。
  • Object.assign(): 忽略enumerable為false的屬性,只拷貝對象自身的可枚舉的屬性。

這四個操作之中,前三個是 ES5 就有的,最後一個Object.assign()是 ES6 新增的。其中,只有for...in會返回繼承的屬性,其他三個方法都會忽略繼承的屬性,只處理對象自身的屬性。實際上,引入“可枚舉”(enumerable)這個概念的最初目的,就是讓某些屬性可以規避掉for...in操作,不然所有內部屬性和方法都會被遍歷到。比如,對象原型的toString方法,以及數組的length屬性,就通過“可枚舉性”,從而避免被for...in遍歷到。

Object.getOwnPropertyDescriptor(Object.prototype, 'toString').enumerable
// false

Object.getOwnPropertyDescriptor([], 'length').enumerable
// false

上面代碼中,toString和length屬性的enumerable都是false,因此for...in不會遍歷到這兩個繼承自原型的屬性。

另外,ES6 規定,所有 Class 的原型的方法都是不可枚舉的。

Object.getOwnPropertyDescriptor(class {foo() {}}.prototype, 'foo').enumerable
// false

總的來說,操作中引入繼承的屬性會讓問題復雜化,大多數時候,我們只關心對象自身的屬性。所以,盡量不要用for...in循環,而用Object.keys()代替。


屬性的遍歷

ES6 一共有 5 種方法可以遍歷對象的屬性:

  1. for...in

    for...in循環遍歷對象自身的和繼承的可枚舉屬性(不含 Symbol 屬性)。

  2. Object.keys(obj)

    Object.keys返回一個數組,包括對象自身的(不含繼承的)所有可枚舉屬性(不含 Symbol 屬性)的鍵名。

  3. Object.getOwnPropertyNames(obj)

    Object.getOwnPropertyNames返回一個數組,包含對象自身的所有屬性(不含 Symbol 屬性,但是包括不可枚舉屬性)的鍵名。

  4. Object.getOwnPropertySymbols(obj)

    Object.getOwnPropertySymbols返回一個數組,包含對象自身的所有 Symbol 屬性的鍵名。

  5. Reflect.ownKeys(obj)

    Reflect.ownKeys返回一個數組,包含對象自身的所有鍵名,不管鍵名是 Symbol 或字符串,也不管是否可枚舉。

以上的 5 種方法遍歷對象的鍵名,都遵守同樣的屬性遍歷的次序規則:

首先遍歷所有數值鍵,按照數值升序排列。
其次遍歷所有字符串鍵,按照加入時間升序排列。
最後遍歷所有 Symbol 鍵,按照加入時間升序排列。

Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
// ['2', '10', 'b', 'a', Symbol()]

上面代碼中,Reflect.ownKeys方法返回一個數組,包含了參數對象的所有屬性。這個數組的屬性次序是這樣的,首先是數值屬性2和10,其次是字符串屬性b和a,最後是 Symbol 屬性。

super 關鍵字

我們知道,this關鍵字總是指向函數所在的當前對象,ES6 又新增了另一個類似的關鍵字super,指向當前對象的原型對象。

const proto = {
  foo: 'hello'
};

const obj = {
  foo: 'world',
  find() {
    return super.foo;
  }
};

Object.setPrototypeOf(obj, proto);
obj.find() // "hello"

上面代碼中,對象obj.find()方法之中,通過super.foo引用了原型對象proto的foo屬性。

註意,super關鍵字表示原型對象時,只能用在對象的方法之中,用在其他地方都會報錯。

// 報錯
const obj = {
  foo: super.foo
}

// 報錯
const obj = {
  foo: () => super.foo
}

// 報錯
const obj = {
  foo: function () {
    return super.foo
  }
}

上面三種super的用法都會報錯,因為對於 JavaScript 引擎來說,這裏的super都沒有用在對象的方法之中。第一種寫法是super用在屬性裏面,第二種和第三種寫法是super用在一個函數裏面,然後賦值給foo屬性。目前,只有對象方法的簡寫法可以讓 JavaScript 引擎確認,定義的是對象的方法。

JavaScript 引擎內部,super.foo等同於Object.getPrototypeOf(this).foo(屬性)或Object.getPrototypeOf(this).foo.call(this)(方法)。

const proto = {
  x: 'hello',
  foo() {
    console.log(this.x);
  },
};

const obj = {
  x: 'world',
  foo() {
    super.foo();
  }
}

Object.setPrototypeOf(obj, proto);

obj.foo() // "world"

上面代碼中,super.foo指向原型對象proto的foo方法,但是綁定的this卻還是當前對象obj,因此輸出的就是world。

對象的擴展運算符

《數組的擴展》一章中,已經介紹過擴展運算符(...)。
ES2018 將這個運算符引入了對象。


解構賦值

對象的解構賦值用於從一個對象取值,相當於將目標對象自身的所有可遍歷的(enumerable)、但尚未被讀取的屬性,分配到指定的對象上面。所有的鍵和它們的值,都會拷貝到新對象上面。

let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x // 1
y // 2
z // { a: 3, b: 4 }

上面代碼中,變量z是解構賦值所在的對象。它獲取等號右邊的所有尚未讀取的鍵(a和b),將它們連同值一起拷貝過來。

由於解構賦值要求等號右邊是一個對象,所以如果等號右邊是undefined或null,就會報錯,因為它們無法轉為對象。

let { x, y, ...z } = null; // 運行時錯誤
let { x, y, ...z } = undefined; // 運行時錯誤

解構賦值必須是最後一個參數,否則會報錯。

let { ...x, y, z } = obj; // 句法錯誤
let { x, ...y, ...z } = obj; // 句法錯誤

上面代碼中,解構賦值不是最後一個參數,所以會報錯。

註意,解構賦值的拷貝是淺拷貝,即如果一個鍵的值是復合類型的值(數組、對象、函數)、那麽解構賦值拷貝的是這個值的引用,而不是這個值的副本。

let obj = { a: { b: 1 } };
let { ...x } = obj;
obj.a.b = 2;
x.a.b // 2

上面代碼中,x是解構賦值所在的對象,拷貝了對象obj的a屬性。a屬性引用了一個對象,修改這個對象的值,會影響到解構賦值對它的引用。

另外,擴展運算符的解構賦值,不能復制繼承自原型對象的屬性。

let o1 = { a: 1 };
let o2 = { b: 2 };
o2.__proto__ = o1;
let { ...o3 } = o2;
o3 // { b: 2 }
o3.a // undefined

上面代碼中,對象o3復制了o2,但是只復制了o2自身的屬性,沒有復制它的原型對象o1的屬性。

下面是另一個例子。

const o = Object.create({ x: 1, y: 2 });
o.z = 3;

let { x, ...newObj } = o;
let { y, z } = newObj;
x // 1
y // undefined
z // 3

上面代碼中,變量x是單純的解構賦值,所以可以讀取對象o繼承的屬性;變量y和z是擴展運算符的解構賦值,只能讀取對象o自身的屬性,所以變量z可以賦值成功,變量y取不到值。ES6 規定,變量聲明語句之中,如果使用解構賦值,擴展運算符後面必須是一個變量名,而不能是一個解構賦值表達式,所以上面代碼引入了中間變量newObj,如果寫成下面這樣會報錯。

let { x, ...{ y, z } } = o;
// SyntaxError: ... must be followed by an identifier in declaration contexts

解構賦值的一個用處,是擴展某個函數的參數,引入其他操作。

function baseFunction({ a, b }) {
  // ...
}
function wrapperFunction({ x, y, ...restConfig }) {
  // 使用 x 和 y 參數進行操作
  // 其余參數傳給原始函數
  return baseFunction(restConfig);
}

上面代碼中,原始函數baseFunction接受a和b作為參數,函數wrapperFunction在baseFunction的基礎上進行了擴展,能夠接受多余的參數,並且保留原始函數的行為。


擴展運算符

對象的擴展運算符(...)用於取出參數對象的所有可遍歷屬性,拷貝到當前對象之中。

let z = { a: 3, b: 4 };
let n = { ...z };
n // { a: 3, b: 4 }
  • 由於數組是特殊的對象,所以對象的擴展運算符也可以用於數組。

    let foo = { ...['a', 'b', 'c'] };
    foo
    // {0: "a", 1: "b", 2: "c"}

    對象的擴展運算符等同於使用Object.assign()方法。

    let aClone = { ...a };
    // 等同於
    let aClone = Object.assign({}, a);

    上面的例子只是拷貝了對象實例的屬性,如果想完整克隆一個對象,還拷貝對象原型的屬性,可以采用下面的寫法。

    // 寫法一
    const clone1 = {
      __proto__: Object.getPrototypeOf(obj),
      ...obj
    };
    
    // 寫法二
    const clone2 = Object.assign(
      Object.create(Object.getPrototypeOf(obj)),
      obj
    );
    
    // 寫法三
    const clone3 = Object.create(
      Object.getPrototypeOf(obj),
      Object.getOwnPropertyDescriptors(obj)
    )

    上面代碼中,寫法一的__proto__屬性在非瀏覽器的環境不一定部署,因此推薦使用寫法二和寫法三。

  • 擴展運算符可以用於合並兩個對象。

    let ab = { ...a, ...b };
    // 等同於
    let ab = Object.assign({}, a, b);
  • 如果用戶自定義的屬性,放在擴展運算符後面,則擴展運算符內部的同名屬性會被覆蓋掉。

    let aWithOverrides = { ...a, x: 1, y: 2 };
    // 等同於
    let aWithOverrides = { ...a, ...{ x: 1, y: 2 } };
    // 等同於
    let x = 1, y = 2, aWithOverrides = { ...a, x, y };
    // 等同於
    let aWithOverrides = Object.assign({}, a, { x: 1, y: 2 });

    上面代碼中,a對象的x屬性和y屬性,拷貝到新對象後會被覆蓋掉。

    這用來修改現有對象部分的屬性就很方便了

    let newVersion = {
      ...previousVersion,
      name: 'New Name' // Override the name property
    };

    上面代碼中,newVersion對象自定義了name屬性,其他屬性全部復制自previousVersion對象。

  • 如果把自定義屬性放在擴展運算符前面,就變成了設置新對象的默認屬性值。

    let aWithDefaults = { x: 1, y: 2, ...a };
    // 等同於
    let aWithDefaults = Object.assign({}, { x: 1, y: 2 }, a);
    // 等同於
    let aWithDefaults = Object.assign({ x: 1, y: 2 }, a);
  • 與數組的擴展運算符一樣,對象的擴展運算符後面可以跟表達式。

    const obj = {
      ...(x > 1 ? {a: 1} : {}),
      b: 2,
    };
  • 如果擴展運算符後面是一個空對象,則沒有任何效果。

    {...{}, a: 1}
    // { a: 1 }
  • 如果擴展運算符的參數是null或undefined,這兩個值會被忽略,不會報錯。

    let emptyObject = { ...null, ...undefined }; // 不報錯
  • 擴展運算符的參數對象之中,如果有取值函數get,這個函數是會執行的。

    // 並不會拋出錯誤,因為 x 屬性只是被定義,但沒執行
    let aWithXGetter = {
      ...a,
      get x() {
        throw new Error('not throw yet');
      }
    };
    
    // 會拋出錯誤,因為 x 屬性被執行了
    let runtimeError = {
      ...a,
      ...{
        get x() {
          throw new Error('throw now');
        }
      }
    };

對象的擴展