对象在js
中是一个重要组成部分,在实际的项目开发中关于对象我们可能会遇到以下但不局限于以下的使用场景
- 创建一个或多个不同对象
- 使用
for-in
循环来遍历对象 - 给已有对象添加新的属性或者删除已有属性
进一步当我们在使用for-in
循环来遍历对象时,为了避免遍历原型链上的属性,会使用hasOwnProperty
方法来过滤继承的属性。因为for-in
会遍历自身及原型链上所有可枚举的属性。
进而言之当创建一个var o = {foo: 'foo'}
对象并对o
进行遍历并没有使用hasOwnProperty
方法进行过滤时指挥遍历到toString
属性。但使用'toString' in o === true
告诉我们toString
也是o
的属性,继承而来的属性,在使用for-in
进行遍历时没有打印出toString
,由此我们可以推测,toString
属性是不可枚举的。
从上面我们可以看到对象是JS
中一个基础而又复杂的数据类型。当我们在项目开发中根据经验教训或者最佳实践来操作对象时,我们应该明白其背后的原理。下面我们就从以下方面(参考但不局限于JS权威指南)对对象进行阐述。
- 对象创建,继承
- 属性的查询,设置及更新
- 属性删除
- 属性检测
- 属性枚举
- 属性
getter
和setter
- 属性的特性
对象创建
一切都皆为对象
在js
中几乎所有类型的值包括原始类型(数字,字符串,布尔值)都可以认为是对象。
在讨论原始类型时这里只讨论字符串类型,数字和布尔值类型的原始值参考字符串类型。
对象的创建
new 构造函数名()
var foo1 = new String('foo');
var foo2 = String('foo');
var foo3 = 'foo'
console.log(foo2 === foo3) // true
console.log(foo1 === foo2) // false
console.log(typeof foo1, typeof foo2, typeof foo3) // object string string
从上面的代码中可以看到使用 new
和构造函数以及直接使用构造函数来创建函数得到的结果是不同的。 直接使用构造函数创建的foo2
和使用字面量创建字符串的foo3
是严格相等的。
下面我们来关注下foo1
.
console.log(foo1['0'], foo1['1'], foo1['2']) //f o o
foo1['0'] = 'b';
console.log(foo1,foo1['0']) //String {"foo"} "f"
Object.getOwnPropertyDescriptor(foo1, '0') //{value: "f", writable: false, enumerable: true, configurable: false}
foo1
是一个对象类型,它具有对象的一切特性,比如访问属性,但在更改foo1
的'0'
属性时,我们发现无法生效。由此可以推测,该属性是只读不可写的。通过getOwnPropertyDescriptor
获取属性描述符可知该属性不可写,可遍历,不可配置(以为着不可删除)。
字面量创建对象
在实际开发中推荐使用字面量形式创建对象,简洁清晰。
当属性名是非法标识符时需要用引号包括,否则推荐属性名不使用引号。
// 创建一个空对象,自身没有属性,但有继承属性
var foo = {}
// 创建一个没有任何属性的空对象
var bar = Object.create(null)
// 创建一个具有自身属性及行为(方法)的对象来描述Tom个体
var Tom = {
age: 12
sex: 'male',
'school-name': 'xxx',
sayHello: function() {
console.log('hello')
}
}
属性查询设置
查询及设置
在JS
中,我们可以随时为对象添加新的属性,也可以更改属性的值。但在设置属性前我们先聊一聊属性的访问,因为这关系到属性的设置。
// 创建一个继承某个对象的函数
function inherit(p) {
if(p == null) throw new Error('param p cannot be null');
if(Object.create) {
return Object.create(p);
}
function F() {}
F.prototype = p;
return new F();
}
var p = {
foo: 'foo'
}
Object.defineProperty(p,'bar', {
value: 'bar',
writable: false,
configurable: true,
enumerable: false
})
var s = inherit(p)
console.log(s) // {}
for(var key in s) {
console.log(s) // foo
}
s.foo; //foo 访问一个存在的属性
s.bar; //bar
s.toString // ƒ toString() { [native code] }
s.foobar // undefined
s.foo = 'newFoo' //更改一个存在的属性
s.bar = 'newBar' //更改一个存在的属性,未生效
s // {foo: 'newFoo'}
s.foobar = 'foobar'// 设置一个未知的属性
s //{foo: "newFoo", foobar: "foobar"}
上面我们创建了一个继承自p
对象的s
对象,s对象自身没有任何属性,只有继承而来的属性foo
bar
以及原型链上顶层属性toString
,其他属性这里就不一一列举了。在设置属性时从自身查找到Object.prototype
属性,如果原型链上的某个原型对象上已有改属性并且改属性不可写即writable:false
时,设置新属性无效,即不会在对象自身添加属性比如newBar
,如果原型上的某个属性时可写的,那么会给对象自身创建一个新的属性进而覆盖原型对象比如newFoo
,如果自身及原型链上所有对象都不包含新设置的属性,直接给对象自身添加新的属性。
对象的可拓展性
isExtensible | preventExtensions
var o1 = {foo: 'foo'};
Object.isExtensible(o1); // false p判断是否可拓展
o1.bar = 'bar';
o1 // {foo: "foo", bar: "bar"}
Object.preventExtensions(o1); // o1 不可拓展
o1.type = 'object' // 不可添加新的属性
o1 //{foo: "foo", bar: "bar"}
o1.foo = 'newFoo'; //可以更改已有的属性
o1 //{foo: "new Foo", bar: "bar"}
delete o1.foo // 可删除属性
o1 // {bar: "bar"}
isSealed | seal
Object.seal
将对象所有自身属性设置为不可配置,但自身属性的可写性不发升改变
var o2 = {foo: 'foo'};
Object.isSealed (o2 ); // false p判断是否密封
o2 .bar = 'bar';
o2 // {foo: "foo", bar: "bar"}
Object.seal(o2 ); // o1 不可拓展
o2 .type = 'object' // 不可添加新的属性
o2 //{foo: "foo", bar: "bar"}
o2 .foo = 'newFoo'; //可以更改已有的属性
o2 //{foo: "new Foo", bar: "bar"}
delete o2 .foo // 无法删除,不可配置
o2 //{foo: "newFoo", bar: "bar"}
isFrozen | freeze
Object.freeze
将对象所有自身属性设置为不可配置,不可写(只读),但不影响存取器属性。
var o3 = {foo: 'foo'};
Object.isFrozen(o3 ); // false p判断是否密封
o3 .bar = 'bar';
o3 // {foo: "foo", bar: "bar"}
Object.freeze(o3 ); // o1 不可拓展
o3 .type = 'object' // 不可添加新的属性
o3 //{foo: "foo", bar: "bar"}
o3 .foo = 'newFoo'; //不可以更改已有的属性
o3 //{foo: "new Foo", bar: "bar"}
delete o3 .foo // 无法删除,不可配置
o3 //{foo: "foo", bar: "bar"}
获取一个对象的属性时,先查看自身,在从原型链上一级一级向上查找,直到
Object.prototype
。
给对象设置自身没有的属性时,先查看原型上是否存在该属性,如果存在且改属性是不可写的,设置新属性无效,否则给对象自身添加新属性从而覆盖从原型上继承的属性。
属性删除
delete
- 只能删除自身属性,不能删除原型上的属性
- 不可配置的属性不可删除
属性检测
检测对象的属性类型有多种方式,比如typeof
,instanceof
,constructor
Object.prototype.tostring.call(value)
typeof 操作符返回值
string number boolean undefined object function symbol
typeof 'foo' // 'string'
typeof 1 //"number"
typeof true //"boolean"
typeof undefined //"undefined"
typeof null //"object"
typeof {} //"object"
typeof function() {} // "function"
typeof [] //"object"
typeof Symbol() //"symbol"
不能准确区分数组的类型,对数组进行操作返回
object
,可以使用Array.isArray(paramer)
或instanceof Array
来判断一个值是否是数组
在js
中,通过构造函数得到的对象成为构造函数的实例,每个实例都具有constructor
属性,指向构造函数。
(1).constructor === Number //true
'1'.constructor === String // true
true.constructor === Boolean // true
({}).constructor === Object // true
([]).constructor === Array // true
(function(){}).constructor === Function
var s = Symbol()
s.constructor === Symbol // true
Object.prototype.toString.call
// 字符串
var val1 = 's'
val1.toString() //"s"
Object.prototype.toString.call(val1) // "[object String]"
// 数字
var val2 = 2
val2.toString() //"2"
Object.prototype.toString.call(val2) // "[object Number]"
// 布尔值
var val3 = true
val3 .toString() //"true"
Object.prototype.toString.call(val3 ) // "[object Boolean]"
// 数组
var val4 = [1,2,3]
val4.toString() //"1,2,3" 等价于Object.join()
Object.prototype.toString.call(val4) // "[object Array]"
// 对象
var val5 = {foo: 'foo'}
val5.toString() //"[object Object]"
Object.prototype.toString.call(val5) //"[object Object]"
// 函数
var val6 = function f() {}
val6 .toString() //"function f() {}"
Object.prototype.toString.call(val6 ) //"[object Function]"
var val7 = Symbol()
val7.toString() //"Symbol()"
Object.prototype.toString.call(val7 ) //"[object Symbol]"
构建一个方法简化判断
function getType(arg1) {
return Object.prototype.toString.call(arg1).slice(8,-1)
}
getType(val1) //"String"
getType(val2) //"Number"
getType(val3) // "Boolean"
getType(val4) // "Array"
getType(val5) // "Object"
getType(val6) //"Function"
getType(null) //"Null"
getType(undefined) // "Undefined"
属性枚举
Object.prototype.propertyIsEnumerable()
检测属性是否可枚举
for-in
遍历自身及原型链上所有可枚举属性,如果自身某个属性不可枚举,原型链上某个属性是可枚举的,那么原型链上可枚举的同名属性不会被遍历出。
var obj= {foo: 'foo'}
Object.defineProperty(obj,'bar', {
value: 'bar',
enumerable: false,
writble: true,
configable: true
})
Object.prototype.bar = 'prototype_bar';
Object.prototype.newbar = 'prototype_newbar';
obj // {foo: "foo", bar: "bar"}
//输出 foo bar,原型链上的原型对象内置属性是不可遍历的,所以不会被输出,比如toString
for(var key in obj) {
console.log(key)
}
Object.keys的实现
Object.keys(obj) // ['foo']
function getkeys(o) {
if(typeof p !== 'object') throw New Error('the argument type must be object');
let props = [];
for(var key in o) {
if o.hasOwnProperty(key) {
props.push(key)
}
}
return props;
}
从Object.keys的类似实现中可以看到Object.keys输出自身可枚举属性,首先是可枚举属性,然后是自身属性,使用了
hasOwnProperty
过滤了继承的属性。
getOwnPropertyNames
输出自身所有属性
Object.getOwnPropertyNames(obj) // ["foo", "bar"]