Advanced Objects in JavaScript

100
TaoAlpha
2014-08-2918447 words53 minutes to read

JS算是我最常用的集中语言之一了, 而随着nodejs的出现, js终于成为了一款贯通前后端的语言~ High five for this!

JS的Object可以说是应用极为广泛! 那么除了我们通常的那些用法, 对Objects还有什么高级用法吗?

========正文====本文来自: Readability Top Reads==========

与通常我们使用JS中Object的方法不同, 本文中涉及的要更加高端. JS的Objects的基础使用中绝大部分都会和使用json一样的简单. 但是, JS同时也提供了更加复杂的工具来创建Objects, 而且更加有趣也更加有意义, 其中很多在现代的浏览器中都已经得到支持了.

本文中最后谈及的ProxySymbol, 是基于ECMAScript 6的一些特性, 目前在跨浏览器方面还不是很完善.

Getters and setters

Getters和Setters存在与JS中已经有一段时间了, 但是通常很少会用到. 我通常还是会使用常规的函数来获取一些属性. 比如, 我通常都是使用如下这样的函数来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* @param {string} prefix
* @constructor
*/
function Product(prefix) {
/**
* @private
* @type {string}
*/
this.prefix_ = prefix;
/**
* @private
* @type {string}
*/
this.type_ = "";
}

/**
* @param {string} newType
*/
Product.prototype.setType = function (newType) {
this.type_ = newType;
};

/**
* @return {string}
*/
Product.prototype.type = function () {
return this.prefix_ + ": " + this.type_;
}

var product = new Product("fruit");
product.setType("apple");
console.log(product.type()); //logs fruit: apple

jsfiddle

利用getter的话, 我就可以简化这一代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/**
* @param {string} prefix
* @constructor
*/
function Product(prefix) {
/**
* @private
* @type {number}
*/
this.prefix_ = prefix;
/**
* @private
* @type {string}
*/
this.type_ = "";
}

/**
* @param {string} newType
*/
Product.prototype = {
/**
* @return {string}
*/
get type () {
return this.prefix_ + ": " + this.type_;
},
/**
* @param {string}
*/
set type (newType) {
this.type_ = newType;
}
};

var product = new Product("fruit");

product.type = "apple";
console.log(product.type); //logs "fruit: apple"

console.log(product.type = "orange"); //logs "orange"
console.log(product.type); //logs "fruit: orange"

jsfiddle

上面的代码明显还是有些罗嗦的, 而且语法显得很不寻常, 但是set,get的好处就是在使用的时候更加易于理解. 后来我发现如下这样的好东西:

1
2
roduct.type = "apple";
console.log(product.type);

比下面这样可读性更强也更容易接受吧:

1
2
product.setType("apple");
console.log(product.type());

虽然直接的获取以及设定用例的属性还是略微有些违反我已经形成的固有习惯. 长久以来我们都在bugs和技术问题的训练中养成了避免直接对用例赋值的'好习惯'. 同时, 还有一点则是为了返回值的问题. 比如注意上例中这两句:

1
2
console.log(product.type = "orange");  //logs "orange"
console.log(product.type); //logs "fruit: orange"

注意上例中,"orange"先输出, 然后输出"fruit: orange". 在赋值命令的return中, getter是不会被触发的. 所以如果想要通过赋值语句来获取属性值是行不通的. 其实对于set部分, return是会被直接忽略的. 所以你即便在setter中加入return this.type; 也是没有意义的. 通常来说赋值后的默认返回值是可以直接使用的, 除非是本身有另一套的getter.


defineProperty

get propertyname() 语法可用于对象标识符, 在前面的例子中, 我曾经给Product.prototype赋予了一个对象标识符. 本来没啥问题, 但是使用这样的对象标识符会导致prototypes之间链接以实现继承变得更加困难. 此时你就可以使用defineproperty而不用对象标识符来创建getters和setters了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* @param {string} prefix
* @constructor
*/
function Product(prefix) {
/**
* @private
* @type {number}
*/
this.prefix_ = prefix;
/**
* @private
* @type {string}
*/
this.type_ = "";
}

/**
* @param {string} newType
*/
Object.defineProperty(Product.prototype, "type", {
/**
* @return {string}
*/
get: function () {
return this.prefix_ + ": " + this.type_;
},
/**
* @param {string}
*/
set: function (newType) {
this.type_ = newType;
}
});

jsfiddle

上面的代码和之前的例子效果完全一样. 于之前的例子不同, defineProperty的第三个参数称为descriptor, 而且其中除了设置setget以外, 还允许你去定制一些属性以及对应值. 你可以利用descriptor参数来创建一些类似常量之类的这种无法被改变和删除的属性.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

var obj = {
foo: "bar",
};


//A normal object property
console.log(obj.foo); //logs "bar"

obj.foo = "foobar";
console.log(obj.foo); //logs "foobar"

delete obj.foo;
console.log(obj.test); //logs undefined


Object.defineProperty(obj, "foo", {
value: "bar",
});

console.log(obj.foo); //logs "bar", we were able to modify foo

obj.foo = "foobar";
console.log(obj.foo); //logs "bar", write failed silently

delete obj.foo;
console.log(obj.foo); //logs bar, delete failed silently

jsfiddle

上例最后两次对foo.bar的更改尝试都以失败结束. 这是因为defineProperty默认阻止对内置属性的改变. 你可以利用configurablewritable来修改这一行为. 如果你使用的是stric mode, 那么错误信息就不会静默而是转化为js错误输出的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14

var obj = {};

Object.defineProperty(obj, "foo", {
value: "bar",
configurable: true,
writable: true,
});

console.log(obj.foo); //logs "bar"
obj.foo = "foobar";
console.log(obj.foo); //logs "foobar"
delete obj.foo;
console.log(obj.test); //logs undefined

jsfiddle

其中, configurable键值允许你控制属性值能否被删除. 它同时还允许你控制属性值能否被修改. 而writable键值则允许你去给属性赋值进行修改.

如果configurable设定为false(默认), 那么你再次调用defineProperty定义同变量的时候就会造成js的错误, 而且会抛出异常而不是静默处理的.

1
2
3
4
5
6
7
8
9
10
11
12
13

var obj = {};

Object.defineProperty(obj, "foo", {
value: "bar",
});


Object.defineProperty(obj, "foo", {
value: "foobar",
});

// Uncaught TypeError: Cannot redefine property: foo

jsfiddle

而如果你设定configurable为true, 那么你就可以再次利用defineProperty对属性值进行修改. 比如你可以对原本不可写的属性进行修改.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

var obj = {};

Object.defineProperty(obj, "foo", {
value: "bar",
configurable: true,
});

obj.foo = "foobar";

console.log(obj.foo); // logs "bar", write failed

Object.defineProperty(obj, "foo", {
value: "foobar",
configurable: true,
});

console.log(obj.foo); // logs "foobar"

jsfilddle;

同时还要注意下, 任何由defineProperty定义的属性都不能在for in循环中出现的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

var i, inventory;

inventory = {
"apples": 10,
"oranges": 13,
};

Object.defineProperty(inventory, "strawberries", {
value: 3,
});

for (i in inventory) {
console.log(i, inventory[i]);
}

jsfiddle

上述循环会输出如下结果:

1
2
3

apples 10
oranges 13

然后我们用enumerable键值来允许属性值在for in循环中出现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

var i, inventory;

inventory = {
"apples": 10,
"oranges": 13,
};

Object.defineProperty(inventory, "strawberries", {
value: 3,
enumerable: true,
});

for (i in inventory) {
console.log(i, inventory[i]);
}

jsfiddle

上述则会输出:

apples 10
oranges 13
strawberries 3

你还可以利用isPropertyEnumerable来测试一个属性是否能够出现在循环中.

1
2
3
4
5
6
7
8
9
10
11
12
13
14

var i, inventory;

inventory = {
"apples": 10,
"oranges": 13,
};

Object.defineProperty(inventory, "strawberries", {
value: 3,
});

console.log(inventory.propertyIsEnumerable("apples")); //console logs true
console.log(inventory.propertyIsEnumerable("strawberries")); //console logs false

jsfilddle

isPropertyEnumerable对于在prototype其它环节定义的属性值也会默认返回false的, 当然对于以其他形式定义的, 只要是属于这个object的属性都会默认返回false的.

最后还有几点关于使用defineProperty的注意项: 结合使用set,get和设定为true的writable键值, 或者是set,getvalue直接放在一起也是错误的做法. 把一个属性设定为一个数型值, 只会把这个数值转化为一个字符串的(这个在所有情况下都是一样的). 在defineProperty中你也是可以把value定义为函数的.

defineProperties

除了defineProperty, Object还有Object.defineProperties. 可以允许你一次性定义以多个属性. 有篇文章对比过两者的区别, 不过就Chrome而言, 两者并无太明显的差别.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

var foo = {}

Object.defineProperties(foo, {
bar: {
value: "foo",
writable: true,
},
foo: {
value: function() {
console.log(this.bar);
}
},
});

foo.bar = "foobar";
foo.foo(); //logs "foobar"

jsfiddle

Object.create

Object.createnew是基本一样的, 都可以允许你创建一个新的对象. 函数本身有2个参数, 一个是对象的prototype, 另一个则是property descriptor, 其的传参方式则和Object.defineProperties完全一致.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

var prototypeDef = {
protoBar: "protoBar",
protoLog: function () {
console.log(this.protoBar);
}
};
var propertiesDef = {
instanceBar: {
value: "instanceBar"
},
instanceLog: {
value: function () {
console.log(this.instanceBar);
}
}
}

var foo = Object.create(prototypeDef, propertiesDef);
foo.protoLog(); //logs "protoBar"
foo.instanceLog(); //logs "instanceBar"

jsfiddle

在property descriptor中传入的属性值会覆盖掉prototype的同属性值.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

var prototypeDef = {
bar: "protoBar",
};
var propertiesDef = {
bar: {
value: "instanceBar",
},
log: {
value: function () {
console.log(this.bar);
}
}
}

var foo = Object.create(prototypeDef, propertiesDef);
foo.log(); //logs "instanceBar"

jsfiddle

设置一个高级类型, 比如array或者object作为Object.create的传参可能会导致一些错误, 因为你将会创建一个所有对象共用的单例(变量会互串,公用单例的对象之间相互影响).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

var prototypeDef = {
protoArray: [],
};
var propertiesDef = {
propertyArray: {
value: [],
}
}

var foo = Object.create(prototypeDef, propertiesDef);
var bar = Object.create(prototypeDef, propertiesDef);

foo.protoArray.push("foobar");
console.log(bar.protoArray); //logs ["foobar"]
foo.propertyArray.push("foobar");
console.log(bar.propertyArray); //also logs ["foobar"]

jsfiddle

当然, 你可以通过初始化propertyArray为null来解决之前的问题. 如此你就可以随便使用任意的array了, 甚至可以做更多的事情, 比如使用getter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

var prototypeDef = {
protoArray: [],
};
var propertiesDef = {
propertyArrayValue_: {
value: null,
writable: true
},
propertyArray: {
get: function () {
if (!this.propertyArrayValue_) {
this.propertyArrayValue_ = [];
}
return this.propertyArrayValue_;
}
}
}

var foo = Object.create(prototypeDef, propertiesDef);
var bar = Object.create(prototypeDef, propertiesDef);

foo.protoArray.push("foobar");
console.log(bar.protoArray); //logs ["foobar"]
foo.propertyArray.push("foobar");
console.log(bar.propertyArray); //logs []

jsfiddle

初始化属性值是一个很便捷的方式. 我通常会比较喜欢在工作中把各项属性值都初始化的. 过去我的代码中经常会出现这样的初始化代码.

之前的例子告诉你: 一旦property descriptor定义好了, 那么其中的所有属性值都自动生成了(PS. 这块翻译表述不很准确, 请参照原文.). 这也是为什么一个数组会跨实例共享的原因. 我同样推荐千万不要在多个属性值调用时过分依赖代码的顺序. 如果你一定要在其他属性调用前初始化属性, 那么也许可以直接在实例中使用Object.defineProperty.

因为使用Object.create并不包含一个constructor的函数, 所以你就不能使用instanceof来测试对象所属. 所以我们用isPrototypeOf来确定它是哪个prototype的对象. 使用形式可以使用constructor的形式:MyFunction.prototype.isPrototypeOf或者直接使用Object.create的第一个参数来调用也是一样的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

function Foo() {
}

var prototypeDef = {
protoArray: [],
};
var propertiesDef = {
propertyArrayValue_: {
value: null,
writable: true
},
propertyArray: {
get: function () {
if (!this.propertyArrayValue_) {
this.propertyArrayValue_ = [];
}
return this.propertyArrayValue_;
}
}
}

var foo1 = new Foo();

//old way using instanceof works with constructors
console.log(foo1 instanceof Foo); //logs true

//You check against the prototype object, not the constructor function
console.log(Foo.prototype.isPrototypeOf(foo1)); //true

var foo2 = Object.create(prototypeDef, propertiesDef);

//can't use instanceof with Object.create, test against prototype object...
//...given as first agument to Object.create
console.log(prototypeDef.isPrototypeOf(foo2)); //true

jsfiddle

isPropertyOf会遍历整个prototype链直到找到符合检验对象的后就会返回true.

1
2
3
4
5
6
7
8
9
10
11
12
13

var foo1Proto = {
foo: "foo",
};

var foo2Proto = Object.create(foo1Proto);
foo2Proto.bar = "bar";

var foo = Object.create(foo2Proto);

console.log(foo.foo, foo.bar); //logs "foo bar"
console.log(foo1Proto.isPrototypeOf(foo)); // logs true
console.log(foo2Proto.isPrototypeOf(foo)); // logs true

jsfiddle

sealing objects, freezing them and preventing extensibility(如何封装对象来阻止其扩展性)

只是因为可以就给Object增加各种任意的属性是非常不好的. 在现代的浏览器和node.js的结合下, 已经可以实现限制整个Object从而限制某些单个的属性的变化了.

Object.preventExtensions, Object.sealObject.freeze三个函数对Object的限制程度依次增加. 在限制模式下, 一旦出现违反其规则的行为就会抛出js的异常错误的, 而在正常的模式下, 这些错误并不影响代码的正常运行的.

Object.preventExtensions将阻止Object中属性的新增. 它不会阻止那些现有可写入属性的改变, 也不会阻止属性的删除. 而且它也不会阻止调用defineProperty来修改已知属性.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

var obj = {
foo: "foo",
};

obj.bar = "bar";
console.log(obj); // logs Object {foo: "foo", bar: "bar"}

Object.preventExtensions(obj);

delete obj.bar;
console.log(obj); // logs Object {foo: "foo"}

obj.bar = "bar";
console.log(obj); // still logs Object {foo: "foo"}

obj.foo = "foobar"
console.log(obj); // logs {foo: "foobar"} can still change values

jsfiddle

Object.seal比preventExtensions更进一步. 它不只会阻止新增属性, 还会阻止修改属性配置以及删除属性. 一旦object被密封后, 你就不能通过defineProperty来对现有属性进行修改了. 如上所说, 一旦你尝试, 就会报错的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

"use strict";

var obj = {};

Object.defineProperty(obj, "foo", {
value: "foo"
});

Object.seal(obj);

//Uncaught TypeError: Cannot redefine property: foo
Object.defineProperty(obj, "foo", {
value: "bar"
});

jsfiddle

你同样也不能删除任何属性了, 即便它们是设定为可配置的也不行(configurable设定为true). 但是你可以修改这些属性的value.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

"use strict";

var obj = {};

Object.defineProperty(obj, "foo", {
value: "foo",
writable: true,
configurable: true,
});

Object.seal(obj);

console.log(obj.foo); //logs "foo"
obj.foo = "bar";
console.log(obj.foo); //logs "bar"
delete obj.foo; //TypeError, cannot delete

jsfiddle

最后, Object.freeze则会让一个object完全锁死. 你不能进行新增, 删除, 修改赋值等任何变化. 同时你也不再能使用defineProperty来对现有属性修改.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

"use strict";

var obj = {
foo: "foo1"
};

Object.freeze(obj);

//All of the following will fail, and result in errors in strict mode
obj.foo = "foo2"; //cannot change values
obj.bar = "bar"; //cannot add a property
delete obj.bar; //cannot delete a property
//cannot call defineProperty on a frozen object
Object.defineProperty(obj, "foo", {
value: "foo2"
});

jsfiddle

如下的这些函数是用来帮助检测Object是否frozen,sealed或者not extensible的:
Object.preventExtensions, Object.sealObject.freeze

valueOf 和 toString

你可以使用valueOftoString来自定义你定义好的Object在js需要一个初始值的时候如何处理.

下面是一个tostring的例子:

1
2
3
4
5
6
7
8
9
10
11
12

function Foo (stuff) {
this.stuff = stuff;
}

Foo.prototype.toString = function () {
return this.stuff;
}


var f = new Foo("foo");
console.log(f + "bar"); //logs "foobar"

jsfiddle

以及一个valueOf的例子:

1
2
3
4
5
6
7
8
9
10
11

function Foo (stuff) {
this.stuff = stuff;
}

Foo.prototype.valueOf = function () {
return this.stuff.length;
}

var f = new Foo("foo");
console.log(1 + f); //logs 4 (length of "foo" + 1);

jsfiddle

但是如果你同时使用tostring和valueOf的话, 可能会得到一些奇怪的结果.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

function Foo (stuff) {
this.stuff = stuff;
}

Foo.prototype.valueOf = function () {
return this.stuff.length;
}

Foo.prototype.toString = function () {
return this.stuff;
}

var f = new Foo("foo");
console.log(f + "bar"); //logs "3bar" instead of "foobar"
console.log(1 + f); //logs 4 (length of "foo" + 1);

jsfiddle

一个更加便捷的方式使用toString就是让你的object支持hash.

1
2
3
4
5
6
7
8
9
10
11
12
13
14

function Foo (stuff) {
this.stuff = stuff;
}

Foo.prototype.toString = function () {
return this.stuff;
}

var f = new Foo("foo");

var obj = {};
obj[f] = true;
console.log(obj); //logs {foo: true}

jsfiddle

getOwnPropertyNames 和 keys

你可以利用Object.getOwnPropertyNames来获得object中所有已定义的属性名称. 如果你熟悉Python的话, 它和python的dictionary类型的keys函数是一样的. 实际上, 也确实有Object.keys这么一个函数. 两者的区别在于, getOwnPropertyNames同时还会遍历到那些无法枚举到的属性, 即它可以返回那些不会出现在for in循环中的属性.

1
2
3
4
5
6
7
8
9
10
11

var obj = {
foo: "foo",
};

Object.defineProperty(obj, "bar", {
value: "bar"
});

console.log(Object.getOwnPropertyNames(obj)); //logs ["foo", "bar"]
console.log(Object.keys(obj)); //logs ["foo"]

jsfiddle

Symbol

Symbol是一个特殊的初始类型, 是在ECMAScript 6中定义的, 会在下一代js中使用. 你可以通过chrome canary以及firefox nightly以及下面的jsfiddle例子中提前感受下(例子本身也只支持这两种浏览器, 且版本不低于本文发表时间: 2014-8).

Symbols可以被用于在Object中创建以及引用属性.

1
2
3
4
5
6
7
8

var obj = {};

var foo = Symbol("foo");

obj[foo] = "foobar";

console.log(obj[foo]); //logs "foobar"

jsfiddle (Chrome Canary and Firefox Nightly only)

Symbols本身是唯一且不可改变的.

1
2
3

//console logs false, symbols are unique:
console.log(Symbol("foo") === Symbol("foo"));

jsfiddle (Chrome Canary and Firefox Nightly only)

你还可以在defineProperty中使用Symbols.

1
2
3
4
5
6
7
8
9
10

var obj = {};

var foo = Symbol("foo");

Object.defineProperty(obj, foo, {
value: "foobar",
});

console.log(obj[foo]); //logs "foobar"

jsfiddle (Chrome Canary and Firefox Nightly only)

属性通过symbols添加到object后将不能在for in循环中调用. 但是可以在hasOwnProperty中正常反馈.

1
2
3
4
5
6
7
8
9
10

var obj = {};

var foo = Symbol("foo");

Object.defineProperty(obj, foo, {
value: "foobar",
});

console.log(obj.hasOwnProperty(foo)); //logs true

jsfiddle (Chrome Canary and Firefox Nightly only)

Symbols不会出现在getOwnPropertyNames的返回值中, 但是Object本身有Object.getOwnPropertySumbols.

1
2
3
4
5
6
7
8
9
10
11
12
13
14

var obj = {};

var foo = Symbol("foo");

Object.defineProperty(obj, foo, {
value: "foobar",
});

//console logs []
console.log(Object.getOwnPropertyNames(obj));

//console logs [Symbol(foo)]
console.log(Object.getOwnPropertySymbols(obj));

jsfiddle (Chrome Canary and Firefox Nightly only)

Symbols在你不只希望一个属性不被偶然中修改, 更不希望它在正常的流程中出现时, 会是一个很好的帮手. 我并没有想到所有symbols的所有潜在用法, 我可以肯定还有很多.

Proxy

这又是一个在ECMAScript 6中添加的新东西. 截至到2014年8月, proxies只能在Firefox中生效. 所以下面这个例子只能在firefox中看了(实际上我也是用firefox做的测试).

Proxies的出现是让我很兴奋的一件事, 因为它可以允许我们轻易获得任何属性. 请看下面例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

var obj = {
foo: "foo",
};
var handler = {
get: function (target, name) {
if (target.hasOwnProperty(name)) {
return target[name];
}
return "foobar";
},
};
var p = new Proxy(obj, handler);
console.log(p.foo); //logs "foo"
console.log(p.bar); //logs "foobar"
console.log(p.asdf); //logs "foobar"

jsfiddle (Firefox only)

上例中, 我们把Object obj 代理到一个变量上. 我们定义了一个handler的对象来和代理后的对象进行交互. 其中的get函数应该很好理解. 它获取了目标对象以及对象的名称. 我们可以利用这个信息来返回任意一个我们希望获得的属性值, 但是为了以防万一, 我会在对象有的属性返回对应的值, 而在所有对象没有的属性我就返回”foobar”. 我喜欢这个函数, 它可以用在很多有趣的地方.

还有个地方可以让proxy大显身手, 就是用于测试. 除了get以外, 你完全可以增加使用set, has以及更多的处理函数. 一旦Proxy得到更多的浏览器支持, 性能稳定后, 我会考虑写一篇更加详细的博文来聊聊Proxy的.

所以说, 其实对于JS的Object, 还是有很多更加高级的用法的. 即便是现在, 也可以有很多强大的属性定义提供使用, 而在未来, 更是无法想象, 尤其是要想到Proxy完全可以改变js的整个写法. 如果你有任何问题或者纠正我的地方, 请通过Twitter来@我, 告诉我~ 我的用户名是@bjorntipling.

Source Link