第六章 对象
讲在开头,先来说说es3和es5,es3大家都支持,es5的话IE>=9.0,9.0不支持严格模式。
对象是Javascript的基本数据类型,除了字符串、数字、true、false、null、undefined之外,Javascript中其他的值都是对象,字符串、数字和布尔值不是对象,但是它们的行为和不可变对象类似。
三个属性特性:ES5之前对象都是可写、可枚举、可配置的
- 可写
- 可枚举
- 可配置
三个对象特性:
- 对象的属性
- 对象的类
- 对象的扩展标记
三类对象和两类属性
- 内置对象:Date、Array、Function、RegExp
- 宿主对象:HTMLElement对象,也可当作内置对象
- 自定义对象
- 自有属性
- 继承属性
6.1 对象的创建方式
对象的创建方式有3种,1.对象直接量{};2.关键字new;3.Object.create()
-
对象直接量{}
通过对象直接量创建的对象都具有同一个原型,并可以通过Object.prototype获得对原型对象的引用。
var empty = {}; var point = { x: 0, y: 0, }; var point2 = { x: point.x, y: point.y + 1, }; var book = { 'main title': 'Javascript', // 属性名有空格,需要用引号 'sub-tilte': '对象', // 属性名中有-,需要用引号 'for': 'all reader', // 属性名是保留字,需要用引号(在es5中,保留字作属性名可以不带引号) author: { // 属性值是个对象 firstname: 'Chen', lastname: 'Jim', } }; -
关键字new
new运算符创建并初始化一个新对象。关键字new后面跟一个构造函数的调用。
通过关键字new和构造函数创建的对象的原型就是构造函数的prototype属性的值。
new Object()创建的原型继承自Object.prototype,和对象直接量创建一样。
var o = new Object(); // 创建一个空对象,和{}一样 var a = new Array(); // 创建一个空数组,和[]一样 var d = new Date(); // 创建一个表示当前时间的Date对象 var r = new RegExp("js"); // 创建一个可以进行模式匹配的RegExp对象 // 自定义构造函数 todo -
Object.create()----ES5中的方法
Object.create()的第一个参数传入的是这个对象的原型,第二个可选参数用以对对象的属性做进一步的说明。第一个参数可以传入null来创建一个没有原型的对象。
// 创建没有原型的对象 var o1 = Object.create(null); // 同{}和new Object()创建的对象 var o2 = Object.create(Object.prototype);es3中可以用以下代码来模拟原型继承。
function inherit (p) { if (p == null) { // p如果是null,就创建了一个没有原型的对象 throw TypeError(); } if (Object.create) { // 如果Object.create函数存在就直接使用 return Object.create(p); } var t = typeof p; if (t !== 'object' && t !== 'function') { // 说明不是对象 throw TypeError(); } function f () {}; // 定义一个空的构造函数 f.prototype = p; // 给构造函数设置一个原型属性p return new f(); // 返回这个构造函数 } /** inherit()函数的其中一个用途是防止库函数无意间修改那些不受你控制的对象。 不是将对象直接作为参数传入函数,而是将它的继承对象作为参数。 当函数读取继承对象的属性并修改时,修改的时继承对象的属性值,并不影响原始对象。 **/ var o = { x: "don't change me" }; library_function(inherit(o)); // 防止对o的意外修改a'a'a'a'a'a'a'a'a'a'a'a'a'a'a'a'a'a'a
原型和原型链
没有原型的对象不多,Object.prototype是其中之一。
例如new Date()创建的对象的属性继承了Date.prototype,Date.prototype的属性又继承了Object.prototype,这一系列链接的原型对象就是“原型链”。
6.2 属性的查询和设置
6.2.1 属性的查询和设置
可以通过点(.)或者方括号([])来查询和设置对象的属性。运算符左侧是一个表达式,它返回一个对象。
对于点(.)来说,右侧必须是以属性名称命名的简单标识符。
对于方括号([])来说,方括号内必须是一个计算结果为字符串的表达式。
var author = book.author;
var name = book['name'];
book.title = '书的标题';
book['sub-title'] = '书的小标题'; // book.sub-title为错误写法,不支持
6.2.2 继承
Javascript对象有自有属性和继承属性。
假设要查询o的属性x,如果o中没有x属性,那么将会继续在o的原型对象中查询属性x,如果原型对象中也没有,但这个原型对象也有原型,再往上一层层找,直到找到x或者找到一个原型是null的对象为止(Object.prototype的原型是null)。这就是之前提到的“原型链”,可以通过“原型链”实现属性的继承。
// inherit()在上面已经写过
var o = {};
o.x = 1;
var p = inherit(o);
p.y = 2;
var q = inherit(p);
q.z = 3;
var s = q.toString(); // toString()方法继承自Object.prototype
console.log(q.x + q.y); // 3,x和y属性分别从o和p继承
/**
如果q的自有属性中有x属性,那么给x属性赋值只会修改q中x属性的值;
如果q的自有属性中没有x属性,那么q会新建一个x属性,如果q的继承属性中有x属性,那么q的x属性会覆盖继承属性中的x属性,但是这并不影响o中x属性值。
*/
q.x = 4;
console.log(o.x); // 1
如果o继承自一个只读属性x,那么赋值操作是不允许的。
属性赋值要么失败,要么创建一个属性,要么在原始对象中设置属性。
但有一个例外,如果o继承自属性x,而这个属性是一个具有setter方法的accessor属性,那么这时将调用setter方法,而不是给o创建一个属性x。
需要注意的是,setter方法是由对象o调用的,而不是定义这个属性的原型对象调用的。因此如果setter方法定义任意属性,这个操作只针对对象o本身,并不修改原型链。
6.2.3 属性访问错误
查询一个不存在的属性,会返回undefined的。
查询一个对象的属性,如果对象不存在,会报类型错误。
只读属性不允许赋值,但是不会报错
// 第一种
var a;
if (o) {
a = o.x
}
// 第二种
a = o && o.x;
// Object.prototype是只读属性
Object.prototype = 0; // 赋值失败,不会报错,Object.prototype的值不会变(ES5严格模式中会报类型错误)
对对象o设置属性p会失败的场景:
- o中的p属性为只读属性,不能给只读属性重新赋值(例外,definePrototype()方法可以对可配置的只读属性重新赋值)
- o的属性p是继承属性,且它是只读的。不能通过同名的自有属性覆盖。
- o不存在自有属性p,o没有setter方法继承属性p,且o的可扩展性为false。
6.3 删除属性
delete运算符只能删除自有属性,不能删除继承属性。
delete表达式删除成功或者没有副作用(删除不存在的属性)返回true,delete后不是属性访问表达式也返回true。
delete不能删除不可配置的对象属性。某些内置对象的属性为不可配置,例如变量声明和函数声明创建的全局对象的属性。
严格模式下,删除不可配置属性会报类型错误;非严格模式以及ES3下,会返回false。
var o = {
x: 1,
};
delete o.x; // 删除成功返回true
delete o.y; // 属性不存在返回true
var a = 1; // 全局对象的属性a是不可配置的
delete this.a; // 全局对象的属性不可删除 返回false
function f() {}
delete this.f; // 全局对象的属性不可删除,返回false
// 非严格模式下删除全局对象的可配置属性,省略全局对象的引用
this.b = 2;
delete b; // // 严格模式下会报错 严格模式下必须写delete this.b;
6.4 检测属性
-
in运算符:检测属性,自有属性和继承继承都包括。in相比!==可以区分不存在的属性和属性存在但值为undefined。
-
hasOwnProperty():检测自有属性
-
propertyIsEnumerable():检测自有可枚举属性
-
"!==":判断一个属性是否为undefined
// 如果对象的自有属性或者继承属性中存在这个属性就返回true var o = { x: 1, }; console.log("x" in o); // true 属性名要使用字符串 console.log("y" in o); // false console.log("toString" in o); // true 继承属性 console.log(o.hasOwnProperty('x')); // true 自有属性 console.log(o.hasOwnProperty('y')); // false 不存在这个属性 console.log(o.hasOwnProperty('toString')); // false 继承属性 console.log(o.propertyIsEnumerable('x')); // true 自有可枚举属性 console.log(o.propertyIsEnumerable('y')); // false console.log(Object.prototype.propertyIsEnumerable('toString')); // false 自有不可枚举属性 console.log(o.x !== undefined); // true console.log(o.y !== undefined); // false o.z = undefined; console.log(o.z !== undefined); // false console.log('z' in o); // true
6.5 枚举属性
遍历属性的方法:
- for/in:遍历对象中所有可枚举属性,包括自有属性和继承属性
- Object.keys():ES5方法,返回一个数组,这个数组由对象中可枚举的自有属性组成。
- Object.getOwnPropertyNames():返回一个数组,这个数组由对象的所有自有属性组成,包括不可枚举属性。
var o = {
x: 1,
y: 2,
z: 3,
};
o.propertyIsEnumerable('toString'); // false
for(p in o) {
console.log(p);
}// x y z 不会打印继承属性'toString'
Object.getOwnPropertyNames(Object.prototype); // 返回了一个长度为12的数组
Object,keys(Object.prototype); // 返回了[]
for(p in o) {
if (!o.hasOwnProperty(p)) continue; // 跳过继承属性
}
for(p in o) {
if (typeof o[p] === 'function') continue; // 跳过方法
}
/**
用来枚举属性的对象工具函数
把p中的可枚举属性复制到o中
如果o和p中含有同名的属性,则覆盖o中的属性
这个函数并不包括getter和setter以及复制属性
**/
function extend(p, o) {
for(prop in p) {// for/in循环对象的可枚举属性
o[prop] = p[prop];
}
return o;
}
/**
将p中的可枚举属性复制至o中,并返回o
如果o和p中有同名属性,o中的属性将不受影响
这个函数并不包括getter和setter以及复制属性
**/
function merge(p, o) {
for(prop in p) {
if (o.hasOwnProperty(prop)) continue; // 判断对象o中是否有此属性prop
o[prop] = p[prop];
}
return o;
}
/**
如果o中的属性在p没有同名属性,则从o中删除这个属性,并返回o
**/
function restrict(p, o) {
for (prop in o) {
if (!(prop in p)) delete o[prop];
}
return o;
}
/**
如果o中的属性在p中存在同名属性,则从o中删除这个属性
**/
function subtract(p, o) {
//for(prop in o) {
// if (prop in p) delete o[prop];
//}
//return o;
for(prop in p) {
delete o[prop]; // 如果prop在o中不存在也不会有副作用
}
return o;
}
/**
返回一个新对象,这个对象同时拥有o和p的属性
如果o和p有同名属性使用p中的属性
**/
function union(p, o) {
return extend(p, extend(o, {})); // extend(o, {});将o中的属性复制到新对象
}
/**
返回一个新对象,这个对象中拥有p和o中共同有的属性
很像p和o的交集,忽略p中的值
**/
function intersection(p, o) {
return restrict(p, extend(o, {}));
}
/**
返回一个数组,这个数组包含的是o中可枚举的自有属性的值
**/
function keys(o) {
if (typeof o !== 'object') return TypeError;
if (Object.keys) {
return Object.keys(o);
}
var arr = [];
for (prop in o) { // 循环o中可枚举属性
// 所以在这判断是否是自有属性就行
//if (prop.propertyIsEnumerable(o)) arr.push(prop); // for/in已过滤非可枚举属性
if (o.hasOwnProperty(prop)) arr.push(prop);
}
return arr;
}
6.6 属性getter和setter
getter和setter定义的属性叫作存取器属性,它不同于数据属性,数据属性只有一个值。
当程序查询存取器属性的值时,JS调用getter方法(无参数),这个方法的返回值就是属性存取表达式的值。
当程序设置存取器属性的值时,JS调用setter方法,将赋值表达式右侧的值当做参数传入setter。
和数据属性不同,存取器不具有可写性。getter控制属性的可读性,setter控制属性的可写性,读取只写属性将返回undefined。
// 对象直接量定义存取器属性
var o = {
data_prop: value,
// 存取器属性都是成对定义的函数 accessor_prop就是存取器属性
get accessor_prop() {},
set accessor_prop() {},
};
var p = {
x: 1.0,
y: 1.0,
get r() {
return Math.sqrt(this.x * this.x, this.y * this.y);
},
set r(newvalue) {
var oldvalue = Math.sqrt(this.x * this.x, this.y * this.y);
var ratio = newvalue/oldvalue;
this.x *= ratio;
this.y *= ratio;
},
// 只读存取器属性
get theta() {
return Math.atan2(this.y, this.x);
}
}
存取器属性和数据属性一样是可以继承的。
var q = inherit(p);
q.x = 1;
q.y = 1;
console.log(q.r);
console.log(q.theta); // r和theta属性是继承来的
存取器属性可以用在对象的属性自增序号和产生随机数。
// 对象的属性自增序号
var serialnum = {
// $暗示它是个私有属性
$n: 0,
get next() {
return this.$n++;
},
set next(n) {
if (n > this.$n) {
this.$n = n;
} else {
throw '序列号的值不能比当前值小';
}
}
};
// 产生随机器
var random = {
get octet() {
return Math.floor(Math.random() * 256); // 0到256的随机整数
},
get unit16() {
return Math.floor(Math.random() * 65536);
}
};
6.7 属性的特性
ES3无法设置对象属性的可写、可枚举、可配置性,因此ES3中创建的对象的属性都是可写、可枚举、可配置的。ES5中查询和设置这些属性特性的API对库的开发者来说非常重要:
- 可以通过API给原型对象添加方法,并将它们设置成不可枚举,这样就可以使这个方法看起来像是内置方法。
- 可以通过API给对象定义不能修改或删除的属性,借此锁定这个对象。
一个属性包含1个名字和4个特性。
数据属性的4个特性分别是它的值、可写性、可枚举性、可配置性。
存取器属性没有值特性和可写性特性,它的4个特性分别是读取(get)、写入(set)、可枚举性、可配置性。
属性描述符
为了实现以上属性特性的设置和查询操作,ES5定义了属性描述符。
数据属性的描述符对象的属性有value、writable、enumerbale、configurable。
存取器属性的描述符对象的属性有get、set、enumerbale、configurable。
除了get和set属性是函数值外,其他都是Boolean值。
/**
Object.getOwnPropertyDescriptor()方法可以获取某个对象特定的自有属性的属性描述符
**/
// 返回 {value: 1, writable: true, enumerable: true, configurable: true}
Object.getOwnPropertyDescriptor({x: 1}, 'x')
/** 获取上面random中的octet
返回 {set: undefined, enumerable: true, configurable: true, get: ƒ}
**/
Object.getOwnPropertyDescriptor(random, 'octet');
Object.getOwnPropertyDescriptor({}, 'x'); // undefined 没有这个属性
Object.getOwnPropertyDescriptor({}, 'toString'); // undefined 继承属性
// 如果要获得继承属性的特性,使用Object.getPrototypeOf()循环原型链
/**
Object.defineProperty()方法可以设置对象特定属性的特性
**/
Object.defineProperty()
// 创建一个不可枚举的数据属性x
var o = {};
Object.defineProperty(o, 'x', {
value: 1,
writable: true,
enumerbale: false,
configurable: true,
});
console.log(o.x); // 1 证明属性是存在的
console.log(Object.keys(o)); // [] 证明属性x不可枚举
// 让属性x变为只读
Object.defineProperty(o, 'x', {
writable: false,
});
o.x = 2; // 严格模式下设置只读属性的值会报TypeError
console.log(o.x); // 1 值未更改,说明只读
// 属性是可配置的,可以通过这种方法设置值
Object.defineProperty(o, 'x', {
value: 2
});
console.log(o.x); // 2
// 修改为存取器属性
Object.defineProperty(o, 'x', {
get: function () {
return 0;
}
})
console.log(o.x); // 0
/**
修改或创建多个属性,需要使用Object.defineProperties()
第一个参数传入对象,第二个对象传入映射表
**/
var p = Object.defineProperties({}, {
x: {
value: 1,
writable: true,
enumerable: true,
configurable: true,
},
y: {
value: 1,
writable: true,
enumerable: true,
configurable: true,
},
r: {
get: function () {
return Math.sqrt(this.x * this.x, this.y * this.y);
},
enumerable: true,
configurable: true,
}
})
对于新创建的属性来说,默认的特性值是false或者undefined,修改已有属性,默认的特性值不会做修改。
这个方法不能修改继承属性。
可写性控制对值特性的修改。
可配置性控制对其他特性是否可以修改(包括属性是否可以删除),但对于可写特性有一点例外。
如果属性是不可配置的,那么数据属性和存取器属性之间不能互转,对于存取器属性来说,不能修改任何属性特性,对于数据属性来说,能修改值特性,且可将可写特性从true修改为false,但不能false修改为true。
/**
将extend作为不可枚举属性添加到Object.prototype上
**/
Object.defineProperty(Object.prototype, 'extend', {
value: function (o) {
var names = Object.getPropertyNames(); // 获取o的所有自有属性
for (var i = 0; i < names.length; i++) {
// 如果属性已经存在 则跳过
if (names[i] in this) continue;
// 获取所有的属性描述符
var desc = Object.getPropertyDescriptor(o[names[i]]);
Object.defineProperty(this, names[i], desc);
}
},
writable: true,
enumerable: false,
configurable: true,
});
6.8 对象的三个属性
对象的三个属性:原型、类和可扩展性。
6.8.1 原型属性
对象的原型属性是用来继承属性的,我们通常称“o的原型属性”为“o的原型”。
原型属性在实例对象创建之初就设置好的。
ES5中可以通过Object.getPrototypeOf()查询实例对象的原型。
在ES3中,则没有与之等价的函数,经常使用表达式o.constructor.prototype来检测一个对象的原型(不可靠,Object.create()创建的对象不是这样的,但Object.create()也是ES5中的方法)。
/**
p.isPrototypeOf(o)来检测p是否是o的原型
**/
var p = {
x: 1,
}
var o = Object.create(p)
p.isPrototypeOf(o); // true
Object.prototype.isPrototypeOf(p); // true
6.8.2 类属性
对象的类属性是一个字符串,表示对象的类型信息。
没有方法可以设置这个属性。
并且只有一个方法可以间接查询这个属性:toString()方法继承自Object.prototype,返回[object class]
可以截取字符串第8位至最后第2位之间的字符,但是很多对象的toString()方法被重写了,为了能调用正确的toString()方法,必须间接的调用Function.call()方法。
// classOf()函数可以返回传递给它的任意对象的类属性
function classOf(o) {
if (o === null) return 'Null';
if (o === undefined) return 'Undefined';
return Object.prototype.toString.call(o).slice(8, -1);
}
var date = new Date();
classOf(date); // "Date"
6.8.3 可扩展属性
ES5可以通过Object.esExtensible(),来判断对象是否是可扩展的。
可以调用Object.preventExtensions()将对象转化为不可扩展。
注意,一旦将对象转化为不可扩展就无法再转化为可扩展。
Object.preventExtensions()只影响对象本身的可扩展性,如果给对象的原型添加新的属性,这个不可扩展对象还是会继承这些新属性的。
可扩展属性的目的是将对象锁定,不受外界的干扰。对象的可扩展性通常和属性的可配置性和可写性结合使用。
Object.seal()不但可以将对象设置为不可扩展,而且还能将对象的自有属性设置为不可配置,比Object.preventExtensions()更加强大。
可以使用Object.isSealed()来检查对象是否封闭。
Object.freeze()不但能完成Object.seal()所做的,还能更近一步的将对象的自有数据属性设置为只读。
使用Object.isFrozen()来检查对象是否冻结。
Object.preventExtensions()、Object.seal()和Object.freeze()这些方法都是ES5中的方法,他们返回传入的对象,可以通过函数嵌套的方式调用它们。
// 创建一个封闭的对象 包括一个冻结的原型和一个不可枚举的属性
var o = Object.seal(
Object.create(Object.freeze({x: 1 }),{
y: {
value: 2,
writable:true,
}
}));
6.9 序列化对象
对象序列化是指将对象转化为字符串,也可将字符串还原为对象。
JSON.stringify()和JSON.parse()用来序列化和还原js对象,都是ES5中的方法。
这2个方法都可以接收第二个可选参数,通过传入需要序列化或还原的属性列表来定制自定义的序列化或还原操作。
var o = {
x: 1,
y: 2,
};
var str = JSON.stringify(o);
var json = JSON.parse(str); // json是o的深拷贝
/**
在这先解释一下深拷贝和浅拷贝
深拷贝就是创建了一个新的对象,这个对象的属性和值和原对象一致,但这两个对象是2个引用地址,因此在深拷贝结束 之后改变o对象的值,不会影响json对象。
浅拷贝就是两个对象的引用地址是一致的,因此改变任意一个对象,都会对另外一个对象造成影响。
使用JSON.stringify()和JSON.parse()来实现深拷贝会有几个坑
1.NaN、Infinity、-Infinity序列化的结果是null,无法深拷贝这些值
2.Date对象序列化的结果是ISO格式的日期字符串,JSON.parse()还原时依然会保留它们的字符串形态。
3.函数、RegExp、Error对象和undefined值不能序列化和还原。
4.JSON.stringify()只能序列化对象可枚举的自有属性,不能序列化的属性会被省略。
**/
JSON.stringify({
x: NaN,
y: Infinity,
z: -Infinity,
}); // {"x":null,"y":null,"z":null}
JSON.parse(JSON.stringify({
x: NaN,
y: Infinity,
z: -Infinity,
})); // {x: null, y: null, z: null}
JSON.stringify(new Date()); // ""2020-02-29T12:13:35.360Z""
JSON.parse(JSON.stringify(new Date())); // "2020-02-29T12:13:48.829Z"
6.10 对象方法
Object.prototype中定义的几个方法:
- toString()方法
- toLocaleString()方法:返回一个表示这个对象的本地化字符串,Date和Number对这个方法做了定制。
- toJSON()方法
- valueOf()方法
6.11 ES6中的对象
6.11.1 属性的简洁表示法
6.11.2 属性名表达式
6.11.3 方法的name属性
6.11.4 属性的可枚举和遍历
6.11.5 属性的可枚举和遍历
Object.getOwnPropertySymbols(obj)
Reflect.ownKeys(obj)
6.11.6 super关键字
this指向函数所在的对象。
super指向当前对象的原型对象。
6.11.7 扩展运算符
6.11.8 链判断运算符
6.11.9 Null判断运算符
6.11.10 Object.is()
ES5中比较2个值是否相等,使用“==”和“===”。
前者会自动转换类型,进行比较。
后者的NaN不等于自身,以及+0等于-0。
Object.is()的出现是为了弥补“===”的问题。
Object.is('Jim','Jim'); // true
Object.is({},{}); // false
Object.is(+0,-0); // false
Object.is(NaN,NaN);// true
+0 === -0 // true
NaN === NaN // false
Object.definePrototype(Object, 'is', {
value: function(x, y) {
if (x === y) {
// 针对+0、-0要返回false 说明x和y是+0、-0,这时返回false
return x !== 0 || 1/x === 1/y;
}
// 针对NaN要返回true 说明x和y都是NaN,这种情况下返回true
return x !== x && y !== y;
}
})
6.11.11 Object.assign()
Object.assign()方法用于对象的合并,将源对象的所有可枚举属性,复制到目标对象。
const target = {
a: 1
};
const source1 = {
b: 2
};
const source2 = {
c: 3
};
Object.assign(target, source1, source2); // {a: 1, b: 2, c: 3}
Object.assign()方法的第一个参数是目标对象,后面的参数都是源对象。
如果目标对象与源对象有同名属性,或者多个源对象之间有同名属性,则后面的属性会覆盖前面的属性。
Object.assign()只拷贝源对象的自身可枚举属性。
Object.assign({x: 1, y: 2}, Object.defineProperty({}, 'z', {
value: 3,
enumerable: false
})); // {x: 1, y: 2}
Object.assign()只有第一层是深拷贝,第二层开始都是浅拷贝。
深拷贝的方法有:
- JSON.stringify、JSON.parse来处理,它的4个缺点上面也提到了。
- JQuery的extend方法
- 自己写一个递归方法处理对象的每一层的每个属性。
常见用途:
- 为对象添加属性
- 为对象添加方法
- 克隆对象
- 合并多个对象
- 为属性指定默认值
6.11.12 Object.getOwnPropertyDescriptors()
在上面ES5中提到了Object.getOwnPropertyDescriptor()方法,它是获取对象的某个自身属性的属性描述符。而Object.getOwnPropertyDescriptors()返回指定对象的所有自身属性的属性描述符。