很多时候我们需要复制目标对象而非借助原型链访问,比如对象拷贝、各类继承方法,这里总结下Js的属性访问方法以及注意事项
可以根据是否在原型链上与可枚举来区分:
获取对象直接包含的属性的方法:
Object.keys(obj) //返回可枚举属性 字符串数组
Object.entries(obj) //返回可以枚举属性 键值对数组
Object.getOwnPropertyNames(obj)
Object.getOwnPropertySymbols(obj)
Object.getOwnPropertyDescriptors(obj)
Object.getOwnPropertyDescriptor(obj,prop)
不仅返回自身属性,还能访问原型链上属性的只有一个方法(语句)
for..in //遍历对象可枚举属性列表
需要注意这些方法的返回值:Object.entries(...)
不仅返回属性还返回值,组成键值对,Object.getOwnPropertyDescriptor(obj,prop)
需要对象以及具体的属性值,返回整个property descriptor对象,Object.getOwnPropertyDescriptors(...)
返回一个property descriptor对象数组。
比较符合使用习惯的是
Object.keys(...)
与Object.getOwnPropertyNames(...)
,通过返回的代表属性的字符串来进行某种操作。
涉及到具体的描述,比如访问器属性,就需要
Object.getOwnPropertyDescriptors(...)
这类方法
除去访问方法,另外还有对应的检测方法(运算符),检测存在性,均返回布尔值:
in
obj.hasOwnProperty(prop) //Object.prototype.hasOwnProperty(...)
obj.propertyIsEnumerable(prop) //Object.prototype.propertyIsEnumerable(...)
我们可以对比这些方法来记忆:
for..in
与 obj.propertyIsEnumerable(prop)
、Object.keys(obj)
: 针对可枚举属性,前者查找原型链
in
与 obj.hasOwnProperty(prop)
:针对所有属性(包括Symbol),前者查找原型链
obj.hasOwnProperty(prop)
与 Object.getOwnPropertyNames(obj)
:针对自身属性,前者可用于属性值为Symbol的情况,而后者需要同类方法Object.getOwnPropertySymbols(obj)
在用这些方法进行访问、取值之前,还有两个重要的方法需要介绍:
Object.assign(target, ...sources)
Object.create(proto, [propertiesObject])
两个方法都创建了对象:assign将可枚举属性的值复制到target,继承属性和不可枚举属性是不能拷贝的,source为多个对象时,相同属性会被后续对象合并;create创建指定原型链的对象,第二个参数指定可枚举属性。
可以开始操作了:
function MyClass() {
SuperClass.call(this);
OtherSuperClass.call(this);
}
// 继承一个类
MyClass.prototype = Object.create(SuperClass.prototype);
// 混合其它
Object.assign(MyClass.prototype, OtherSuperClass.prototype);
// 重新指定constructor
MyClass.prototype.constructor = MyClass;
MyClass.prototype.myMethod = function() {
// do a thing
};
上面是MDN里关于混入的例子,实际上拷贝或者继承的用法核心便是如此,借用或者拷贝属性,具体一点可以是这样:
function copy(target, source, overlay) {
for (var key in source) {
if (source.hasOwnProperty(key)
&& (overlay ? source[key] != null : target[key] == null)
) {
target[key] = source[key];
}
}
return target;
}
function mixin(target, source) {
for (var key in source) {
if (!(var key in target)) {
target[key] = source[key];
}
}
return target;
}
通过for.. in语句 获得可枚举属性,并筛选出直接属性,当然透过target[key] = source[key];
也清楚这和Object.assign()
一样只能浅拷贝。
或者更具体的继承用法:
function inherits(clazz, baseClazz) {
var clazzPrototype = clazz.prototype;
function F() {}
F.prototype = baseClazz.prototype;
clazz.prototype = new F();
// clazz.prototype = Object.create(baseClazz.prototype)
for (var prop in clazzPrototype) {
clazz.prototype[prop] = clazzPrototype[prop];
}
clazz.prototype.constructor = clazz;
clazz.superClass = baseClazz;
}
是否把基类原型链上的方法拷贝过来、是否覆盖、是否只往上追溯一层原型链这些都视具体的应用场景而定。inherits会倾向于继承关系(保持原型链的联系),copy用于混入某些属性(组合)。
接下来总结一些注意事项
参考MDN上的分类,有这些容易忽略的情况:属性是否为访问描述符,原始类型包装,原生方法覆盖,以及异常处理是否中断执行。
1.Object.assign()
使用了方法使用源对象的[[Get]]和目标对象的[[Set]],所以源对象的属性为访问器的话,只能获得[[Get]]的值,如果要完整拷贝需要结合Object.getOwnPropertyDescriptor()
和Object.defineProperty()
2.Object.assign()
的source参数可以是基本值,基本值会封装为对象,null 和 undefined 会被忽略,并且只有字符串的包装对象才可能有自身可枚举属性。
- 数据描述符与访问描述符的enumerable属性默认为 false。如果使用直接赋值的方式创建对象的属性,则这个属性的enumerable为true,这是相对于
Object.defineProperty(...)
方法而言,比如
const obj = {
foo: 1,
get bar() {
return 2;
}
};//"foo"与"bar"均可枚举可配置
var o = {};
Object.defineProperty(o, "a", { value : 1 });
//"a"不可枚举不可写不可配置
- 原生方法可能被自定义的同名函数覆盖,这时候可以直接使用切换上下文的原生方法
var foo = {
hasOwnProperty: function() {
return false;
},
bar: 'Here be dragons'
};
({}).hasOwnProperty.call(foo, 'bar'); // true
// 也可以使用 Object 原型上的 hasOwnProperty 属性
Object.prototype.hasOwnProperty.call(foo, 'bar'); // true
-
Object.assign
不会跳过那些值为null
和undefined
的源对象,在出现错误的情况下,例如,如果属性不可写,会引发TypeError,如果在引发错误之前添加了任何属性,则可以更改target对象。Object.create
如果propertiesObject
参数是null
或非原始包装对象,同样抛出一个TypeError。
6.拷贝中常见等号赋值的操作如target[key] = source[key]
,clazz.prototype[prop] = clazzPrototype[prop]
,这个表达式同时有[[Get]]和[[Put]]的操作,需要注意属性设置[[Put]]可能发生屏蔽的状况:如果target本来就具有key属性,那么赋值语句只是修改;如果没有,就在其[[Prototype]]上寻找对应key,①找到并且key为可写的话,target会新增屏蔽属性,如果只读则会被忽略(严格模式下报错),②key为setter,那么target不会新增key属性,只是会调用setter。