[JS] You Don't Know JavaScript [this & Object Prototypes] - Object [下]

前言

Object [上]中我们介绍了物件的宣告、型态、拷贝等等特性,接下来我们继续介绍物件中都有哪些特性。

Property Descriptors

在ES5之前JS没有提供一个可以区分物件属性特徵的方法,但在ES5中可以透过property descriptor观察物件属性的特性。

var myObject = {
	a: 2
};

Object.getOwnPropertyDescriptor( myObject, "a" );
// {
//    value: 2,
//    writable: true,
//    enumerable: true,
//    configurable: true
// }

我们对myObject中的属性a使用getOwnPropertyDescriptor,可以得到除了value的另外三个特质:writableenumerableconfigurable

既然我们可以拿到这个数性的特质,那们我们也可透过Object.defineProperty(...)来添加新的特性或是修改原本的特性。

var myObject = {};

Object.defineProperty( myObject, "a", {
	value: 2,
	writable: true,
	configurable: true,
	enumerable: true
} );

myObject.a; // 2

一班如果你要加入一个正常的属性到物件中是不必要使用Object.defineProperty这个方法,除非你想新增特殊型态的属性。

Writable

此特性代表你是否能够更改属性的值。

var myObject = {};

Object.defineProperty( myObject, "a", {
	value: 2,
	writable: false, // not writable!
	configurable: true,
	enumerable: true
} );

myObject.a = 3; // can't re-assignment
myObject.a; // 2

对於myObject中的a属性设定为不可写入则我们无法再次赋值给他,如果这个程序在严格模式下执行,会掷出TypeError

"use strict";

var myObject = {};

Object.defineProperty( myObject, "a", {
	value: 2,
	writable: false, // not writable!
	configurable: true,
	enumerable: true
} );

myObject.a = 3; // TypeError : Cannot change a non-writable property.

Configurable

Configurable特性控制着我们能否使用defineProperty(...)重新改变属性的特性。

var myObject = {
	a: 2
};

myObject.a = 3;
myObject.a;					// 3

Object.defineProperty( myObject, "a", {
	value: 4,
	writable: true,
	configurable: false,	// not configurable!
	enumerable: true
} );

myObject.a;					// 4
myObject.a = 5;
myObject.a;					// 5

Object.defineProperty( myObject, "a", {
	value: 6,
	writable: true,
	configurable: true,
	enumerable: true
} ); // TypeError

一旦将物件属性的configurable设定为false後,就算不是处於严格模式下,再次尝试更改数性的设置就会掷出TypeError,这意味着当你设置属性的configurable为false後就不可逆转

当你将这个属性的configurable设定为false後可以避免这个属性被delete操作符移除。

var myObject = {
	a: 2
};

myObject.a;				// 2
delete myObject.a;
myObject.a;				// undefined

// configurable:false
Object.defineProperty( myObject, "a", {
	value: 2,
	writable: true,
	configurable: false,
	enumerable: true
} );

myObject.a;				// 2
delete myObject.a;
myObject.a;				// 2

在JS中delete操作符仅用於移除物件的属性,如果一个物件中的属性引用的是某一个物件/函数的最後一个现存引用,那们当你delete这个属性那们就代表移除了最後一个引用,而这个没有被任何地方引用的物件/函数则会被回收掉,但如果还有其他地方引用则只是单纯地从这个物件中移除他们的Reference总结来说delete仅仅是移除物件的属性而已

Enumerable

Enumerable特性决定了我们是否可以对这个元素进行列举操作(for...in),如果将这个特性设定为false则for...in loop终究无法读取到这个属性

Object.defineProperty( myObject, "a", {
	value: 2,
	writable: true,
	configurable: ture,
	enumerable: false
} );

myObject.b = 2;
myObject.b = 4;

for(let prop in myObject){
    console.log(prop); // disable property a
}
// b
// c

Immutability

有时候我们在开发专案的时候会需要建立一些不希望被更改的物件,ES5中提供了一些方法可以让我做到让物件保持不被更改,但是直得注意的是:这些方法都是浅层不变性,这意味着他只会固定住本体物件和他的直属属性性质,但是如果这个被固定住的物件中有引用其他物件的属性(阵列、物件、函数),那麽这些被Reference的物件属性并不会跟着被锁定住

Object Constant

通过将物件属性writableconfigurable都设定为false让我们的物件保持不能被更改的状况。

var myObject = {};

Object.defineProperty( myObject, "FAVORITE_NUMBER", {
	value: 42,
	writable: false,
	configurable: false
} );

Prevent Extensions

如果要禁止添加属性到我们的物件中,可以使用Object.preventExtensions(...)

var myObject = {
	a: 2
};

Object.preventExtensions( myObject );

myObject.b = 3;
myObject.b; // undefined

在非严格模式下b的属性添加会失败,否则会掷出TypeError。

Seal

使用Object.seal(..)创建一个被封印的物件,实际上是将这个物件使用Object.preventExtensions(...)固定住不允许加入其他属性在对物件中的每一个属性标记为configurable:false让物件中的元素也不能被删除或重新配置,但是可以对现有存在的值进行修改

Freeze

使用Object.freeze(...)创建一个冻结的物件,实际上是将物件使用Object.seal(..)使他不能添加、删除、重新配置新的属性,再对每个属性使用writable:false让物件属性的值不能被更改,这个方法是最高级别的不可变性。


[[Get]]

关於对於物件属性的访问有个重要的细节。

var myObject = {
	a: 2
};

myObject.a; // 2

使用myObject.a来访问物件中的a属性,但这个操作不只是在物件中查找名称为a属性,实际上他会先在myObject上执行一个[[Get]]操作,他会检查物件并在物件中寻找有没有这个被请求的属性名称,如果找到就返回相对应的值,如果没能在物件中找到属性则会做另一个重要的事情(遍历[prototype]链),如果通过任何方法都找不到这个属性的时候便会返回undefined

var myObject = {
	a: 2
};

myObject.b; // undefined

[[Put]]

既然从一个属性中取得值存在一个内部定义的[[Get]]方法,那麽也会有一个内部定义的[[Put]]。

调用[[Put]]时会根据几个因素表现不同的行为,影响最大的辨识属性是否已经在存在於物件,如果属性存在[[Put]]会检查:

  1. 如果这个属性是访问器描述符号(Getters/Setters)中的Setters,则直接调用Setters。
  2. 如果这个属性的writable是false,在非严格模式下无声的失败,否则掷出TypeError。
  3. 不符合以上两个则像正常赋值一样设置属性。

如果这个属性不存在於物件中则[[Put]]操作会更复杂,我们在後面会详细讲解。


Getters & Setters

ES5中新增了一个方法来覆盖这些默认操作的一部分,不是针对每个属性而不是物件,就是通过getterssetters,Getter 是用来取得指定属性的值的方法而Setter是用来设定指定属性的值的方法

var myObject = {
	// define a getter for `a`
	get a() {
		return 2;
	}
};

myObject.a = 3;
myObject.a; // 2

我们仅为a定义了一个getter,就算之後向再次对a赋值也不会成功,他会无声的将赋予的值丢弃而不会掷出错误,我们可以多新增一个setters让我们可以更改a的值。

var myObject = {
	// define a getter for `a`
	get a() {
		return this.b;
	},

	// define a setter for `a`
	set a(val) {
		this.b = val * 2;
	}
};

myObject.a = 2;
myObject.a; // 4

当我们重新赋予值给a,他会调用myObject中的set(...),他将我们给的值*2後暂存在一个变量b,之後再透过get(...)将这个值传回给a。


Existence

我们可以透过in操作符来确认我的数性是否有存在在物件中。

var  myObject  =  { 
	a : undefined 
} ;

console.log(myObject.a); // undefined
console.log(myObject.b); // undefined

( "a"  in  myObject ) ; 				// true 
( "b"  in  myObject ) ; 				// false

myObject . hasOwnProperty (  "a"  ) ; 	// true 
myObject . hasOwnProperty (  "b"  ) ; 	// false

虽然myObject.amyObject.b都是undefined,但是通过inhasOwnProperty(...)就可以确认数性是否存在於物件当中。

Enumeration

前面我们在介绍属性的特性的时候有稍微介绍什麽是enumerable,现在我们来更仔细的介绍一下这个特性。

var myObject = { };

Object.defineProperty(
	myObject,
	"a",
	{ 
        value: 2,
        enumerable: true,  // make `a` enumerable, as normal
    }
);

Object.defineProperty(
	myObject,
	"b",
	{ 
        value: 3
        enumerable: false, // make `b` NON-enumerable 
    }
);

myObject.b; // 3
("b" in myObject); // true
myObject.hasOwnProperty( "b" ); // true

// .......

for (let prop in myObject) {
	console.log( prop );
}
// "a"

我们在myObject中建立了两个属性(a与b),我们使用hasOwnProperty(...)确认b是确实存在於myObject中,但是由於他的enumerable设定为false,所以使用for...in无法将它打印出来。

除了使用for...in测试数性是否为enumerable,也可以使用propertyIsEnumerable(..)直接测试这个属性是否存在於物件中并且他的enumerable为何。

var myObject = { };

Object.defineProperty(
	myObject,
	"a",
	{ 
        value: 2,
        enumerable: true,  // make `a` enumerable, as normal
    }
);

Object.defineProperty(
	myObject,
	"b",
	{ 
        value: 3
        enumerable: false, // make `b` NON-enumerable 
    }
);

myObject.propertyIsEnumerable( "a" ); // true
myObject.propertyIsEnumerable( "b" ); // false
myObject.propertyIsEnumerable( "c" ); // false

Object.keys( myObject ); // ["a"]
Object.getOwnPropertyNames( myObject ); // ["a", "b"]

也可以使用Object.keys(..)他会返回所有enumerable = ture的属性。


Iteration

在阵列中迭代所有值得方法是使用标准的for loop

var myArray = [1, 2, 3];

for (let i = 0; i < myArray.length; i++) {
	console.log( myArray[i] );
}
// 1 2 3

上面的程序中并不是迭代了阵列中的值,而是迭代了阵列的index,而我们透过迭代获得的index取得阵列中对应的数(muyArray[i])。

在ES5中提供了一些对於阵列的迭代方法,forEach(...)every(...)some(...)

  • forEach(...)会迭代阵列中的所有值并将这些值分别带入callback function中
  • every(...)会持续迭代到最後或着return一个false
  • some(...)会持续迭代到最後或着return一个true

如果想直接得到迭代值而不是阵列的index,在ES6中提供了一个新语法for...of它可以用来迭代阵列或物件。

const myArray = [1, 2, 3];

for(let val of myArray){
    console.log(val);
}
// 1
// 2
// 3

for...of被要求要迭代的东西需要提供一个迭代器物件,每一次循环中都会调用这个迭代器物件的next(...) method 。

在阵列中拥有内建的迭代器物件(@@iterator)所以可以轻易地使用for...of进行迭代,我们也可手动建立一个@@interator来看看他是如何运作的。

var myArray = [ 1, 2, 3 ];
var it = myArray[Symbol.iterator]();

it.next(); // { value:1, done:false }
it.next(); // { value:2, done:false }
it.next(); // { value:3, done:false }
it.next(); // { done:true }

迭代器的next(...) return 一组物件{value: ... , done: ...},物件中的value属性代表当前迭代着值而done则代表是否迭代完成。


结论

本章节中我们介绍了物件更多的特质,属性的特性、如何将物件变为不可变、物件中的取值与赋值等等,我们来做一些总结:

  • 物件中的数性都有三种特性:
    • writable:控制这个属性是否能被更改。
    • Configurable:控制这个属性是否能更改特性配置。
    • Enumerable:控制这个属性是否能被迭代。
  • 当我们需要建立一个不可变的物件时,可以透过以下办法达到:
    • 将属性的 writable与configurable 设定为false,让这个属性不能被更改。
    • 使用Object.preventExtensions(...)可以防止物件添加新属性。
    • 使用Object.seal(...)可以防止添加新属性并且将数性标记为configurable:false(即不可更改属性设置),但是可以更改现有的属性的值
    • 使用Object.freeze(...)可以建立最高级别的不可变物件,即不可新增属性也不可更改现有属性。
  • 当访问物件中的属性则会调用[[Get]],会在物件中找到这个属性的名字,若找到则回传相对应的值,若没有则回传undefined。
  • 当设定物件的属性会调用[[Put]]:
    • 当属性是setter则调用setter。
    • 当这个属性的writable = false,在严格模式下掷出TypeError否则无声的失败。
    • 不符合上面两点则正常赋值。
  • Getter 是用来取得指定属性的值的方法而Setter是用来设定指定属性的值的方法
  • 可以使用inhasOwnProperty(...)检查属性是否存在於物件中。
  • 可以使用propertyIsEnumerable(...)测试数性是否存在於物件中别且检查这个属性使否是可迭代的。
  • 可以使用Object.keys(...)他将会返回物件中所有可迭代的属性。
  • 可以使用for...of迭代阵列中的值。
  • for...of迭代实际上是执行阵列中内建@@iteratornext(...)

参考文献:You Don't Know JavaScript


<<:  (33)试着学 Hexo-番外篇之更新 NexT 主题

>>:  GDPR

【Day 05】format 函式

str.format 是在 python 中常用的字串格式化招式,可以控制想要显示的资料型态、正负号...

JavaScript Day 15. every() 与 some()

在所有的阵列方法里还有 every() 跟 some() ,听说是很冷门的两种方法,确实我在练习上也...

Day 16 「听从你的蜥蜴脑」单元测试、Code Smell 与重构 - If 篇

你有听过「蜥蜴脑」吗?如果你读过 The Pragmatic Programmer,你应该还有印象。...

android studio 30天学习笔记-day 6-介绍retrofit(二)

今天要来了解上一篇的各个步骤 1.创建资料类别 有个快速生成data class的插件,可以从Fil...

沟通这回事:冰山理论

前言 今天跟大家分享「冰山理论」,是在探讨心理与沟通上的常见主题,试着分享我对冰山的概略理解,若有什...