ECMA-262 将对象定义为一组属性的无序集合。严格来说,这意味着对象就是一组没有特定顺序的值。对象的每个属性或方法都由一个名称来标识,这个名称映射到一个值。正因为如此(以及其他还未 讨论的原因),可以把 ECMAScript 的对象想象成一张散列表,其中的内容就是一组键/值对,值可以是数据或者函数
理解对象
以前创建对象的方法通常是给Object创建一个新的实例,在给他添加属性和方法
javascript
let obj = new Object()
obj.name = '张三'
obj.age = 18
obj.say = ()=>{
consloe.log('hello world')
}
上面的代码我们给obj对象添加了属性和方法,现在我们一般使用对象字面量
的方式创建对象
javascript
let obj = {
name:'张三',
age:18,
say(){
consloe.log('hello world')
}
}
这种方式创建的obj和上面的obj是一样, 它们的属性和方法都一样。
属性的类型
属性分两种:数据属性
和访问器属性
。
- 数据属性
数据属性包含一个保存数据值的位置。值会从这个位置读取,也会写入到这个位置。数据属性有 4 个特性描述它们的行为。
- [[Configurable]]:表示属性是否可以通过 delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。默认为true
- [[Enumerable]]:表示属性是否可以通过for-in循环。 默认为true
- [[Writable]]: 表示属性的值是否可以被修改 。默认为true
- [[Value]]: 包含属性实际的值。默认为undefined
如同上面代码一样将属性显示的添加到对象,这里的4个属性都会被设置为默认值。如果我们想修改属性的默认值,可以通过Object.defineProperty()
方法。 这个方法接收 3 个参数: 要给其添加属性的对象、属性的名称和一个描述符对象。最后一个参数,即描述符对象上的属性可以包 含:configurable、enumerable、writable 和 value,跟相关特性的名称一一对应。
注意:严格模式下才会抛出错误,非严格模式调用时无效果
javascript
Object.defineProperty(person,'name',{
configurable:false,
enumerable:false,
writable:false,
value:'张三'
})
//delete person.name // Cannot delete property 'name' of #<Object>
//person.name = '法外狂徒' //Cannot assign to read only property 'name' of object '#<Object>'
for (const key in person) {
console.log('key',key);// 未执行
}
注意:在使用Object.defineProperty()
的时候,如果没有指定 Configurable
、Enumerable
、 writable
的值,那么都默认为false
访问器属性
访问器属性不包含数据值。相反,它们包含一个获取(getter)函数和一个设置(setter)函数,不 过这两个函数不是必需的。在读取访问器属性时,会调用获取函数,这个函数的责任就是返回一个有效 的值。在写入访问器属性时,会调用设置函数并传入新值,这个函数必须决定对数据做出什么修改。访问器属性有 4 个特性描述它们的行为。
- [[Configurable]]:表示属性是否可以通过 delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。默认为true
- [[Enumerable]]:表示属性是否可以通过for-in循环。 默认为true
- [[Get]]: 获取函数,在读取属性时调用。默认值为 undefined。
- [[Set]]: 设置函数,在写入属性时调用。默认值为 undefined。
javascript
let book = {
year_: 2017,
edition: 1
}
Object.defineProperty(book, 'year', {
get() {
return this.year_
},
set(newValue) {
if (newValue > 2017) {
this.year_ = newValue
this.edition += newValue - 2017
}
}
})
book.year = 2018
console.log(book.edition) //2
这里的year
被定义为一个访问器属性,这是访问器属性的典型使用场景,即设置一个属性 值会导致一些其他变化发生;获取函数和设置函数不一定都要定义。只定义获取函数意味着属性是只读的,尝试修改属性会被忽略。\
定义多个属性
在一个对象上同时定义多个属性的可能性是非常大的。为此,ECMAScript 提供了Object.defineProperties()方法。这个方法可以通过多个描述符一次性定义多个属性。它接收两个参数:要为之添加或修改属性的对象和另一个描述符对象,其属性与要添加或修改的属性一一对应
javascript
let book = {}
Object.defineProperties(book, {
year_: {
value: 2017
},
edition: {
value: 1
},
year: {
get() {
return this.year_
},
set(newValue) {
if (newValue > 2017) {
this.year_ = newValue
this.edition += newValue - 2017
}
}
}
})
在 book 对象上定义了两个数据属性 year_和 edition,还有一个访问器属性 year。 最终的对象跟上一个例子中的一样。唯一的区别是所有属性都是同时定义的,并且数据属性的 configurable、enumerable 和 writable 特性值都是 false
读取属性的特性
使用 Object.getOwnPropertyDescriptor()方法可以取得指定属性的属性描述符。这个方法接收两个参数:属性所在的对象和要取得其描述符的属性名。返回值是一个对象,对于访问器属性包含configurable、enumerable、get 和 set 属性,对于数据属性包含 configurable、enumerable、 writable 和 value 属性。
javascript
let book = {}
Object.defineProperties(book, {
year_: {
value: 2017
},
edition: {
value: 1
},
year: {
get: function () {
return this.year_
},
set: function (newValue) {
if (newValue > 2017) {
this.year_ = newValue
this.edition += newValue - 2017
}
}
}
})
let descriptor = Object.getOwnPropertyDescriptor(book, "year_");
console.log(descriptor.value); // 2017
console.log(descriptor.configurable); // false
console.log(typeof descriptor.get); // "undefined"
let descriptor = Object.getOwnPropertyDescriptor(book, "year");
console.log(descriptor.value); // undefined
console.log(descriptor.enumerable); // false
console.log(typeof descriptor.get); // "function"
ECMAScript 2017 新增了 Object.getOwnPropertyDescriptors()静态方法。这个方法实际上 会在每个自有属性上调用 Object.getOwnPropertyDescriptor()并在一个新对象中返回它们。
javascript
let book = {}
Object.defineProperties(book, {
year_: {
value: 2017
},
edition: {
value: 1
},
year: {
get: function () {
return this.year_
},
set: function (newValue) {
if (newValue > 2017) {
this.year_ = newValue
this.edition += newValue - 2017
}
}
}
})
console.log(Object.getOwnPropertyDescriptors(book))
// {
// edition: {
// configurable: false,
// enumerable: false,
// value: 1,
// writable: false
// },
// year: {
// configurable: false,
// enumerable: false,
// get: f(),
// set: f(newValue),
// },
// year_: {
// configurable: false,
// enumerable: false,
// value: 2017,
// writable: false
// }
// }
对象合并
ECMAScript 6 专门为合并对象提供了 Object.assign()方法。此方法接受一个目标对象和一个或多个源对象参数,将源对象上的属性复制到目标对象
javascript
let obj = {name:'张三'}
let obj1 = {}
let result = Object.assign(obj1,obj)
console.log(result) // {name:'张三'}
console.log(obj1) // {name:'张三'}
console.log(obj1===result) //true
Object.assign()实际上对每个源对象执行的是浅复制。如果多个源对象都有相同的属性,则使用最后一个复制的值。此外,从源对象访问器属性取得的值,比如获取函数,会作为一个静态值赋给目标对象。换句话说,不能在两个对象间转移获取函数和设置函数
set() 方法并没有合并到obj1对象上。
当有相同属性时,使用后一个复制的值
javascript
dest = {};
src = { a: {} };
Object.assign(dest, src);
// 浅复制意味着只会复制对象的引用
console.log(dest); // { a :{} }
console.log(dest.a === src.a); // true
Object.assign()实际上对每个源对象执行的是浅复制
增强的对象语法
- 属性简写
javascript
let name = 'Matt';
let person = {
name: name
};
//简写为
let person = {
name
}
- 可计算属性
在引入可计算属性之前,如果想使用变量的值作为属性,那么必须先声明对象,然后使用中括号语法来添加属性
javascript
const nameKey = 'name';
const ageKey = 'age';
const jobKey = 'job';
let person = {};
person[nameKey] = 'Matt';
person[ageKey] = 27;
person[jobKey] = 'Software engineer';
console.log(person); // { name: 'Matt', age: 27, job: 'Software engineer' }
有了可计算属性,就可以在对象字面量中完成动态属性赋值
javascript
const nameKey = 'name';
const ageKey = 'age';
const jobKey = 'job';
let person = {
[nameKey]: 'Matt',
[ageKey]: 27,
[jobKey]: 'Software engineer'
};
console.log(person); // { name: 'Matt', age: 27, job: 'Software engineer' }
因为被当作 JavaScript 表达式求值,所以可计算属性本身可以是复杂的表达式,在实例化时再求值
javascript
const nameKey = 'name';
const ageKey = 'age';
const jobKey = 'job';
let uniqueToken = 0;
function getUniqueKey(key) {
return `${key}_${uniqueToken++}`;
}
let person = {
[getUniqueKey(nameKey)]: 'Matt',
[getUniqueKey(ageKey)]: 27,
[getUniqueKey(jobKey)]: 'Software engineer'
};
console.log(person); // { name_0: 'Matt', age_1: 27, job_2: 'Software engineer' }
- 简写方法名
javascript
let person = {
sayName: function(name) {
console.log(`My name is ${name}`);
}
};
person.sayName('Matt'); // My name is Matt
//简写
let person = {
sayName(name) {
console.log(`My name is ${name}`);
}
};
person.sayName('Matt'); // My name is Matt
简写方法名与可计算属性键相互兼容
javascript
const methodKey = 'sayName';
let person = {
[methodKey](name) {
console.log(`My name is ${name}`);
}
}
person.sayName('Matt'); // My name is Matt
对象解构
ECMAScript 6 新增了对象解构语法,可以在一条语句中使用嵌套数据实现一个或多个赋值操作。简 单地说,对象解构就是使用与对象匹配的结构来实现对象属性赋值。 解构赋值不一定与对象的属性匹配。赋值的时候可以忽略某些属性,而如果引用的属性不存在,则 该变量的值就是 undefined:
javascript
// 不使用解构
let obj = {name:'张三',age:18,gender:'男'}
let name = obj.name
let age = obj.age
let gender = obj.gender
console.log(name,age,gender) // 张三 18 男
// 使用解构
let obj = {name:'张三',age:18,gender:'男'}
const {name,age,gender} = obj
console.log(name,age,gender) // 张三 18 男
// 解构重命名
const {name:n,age:a,gender:g} = obj
console.log(n,a,g) // 张三 18 男
// 引用的属性不存在
const {address} = obj
console.log(address) // undefined
// 定义默认值
const {address='龙华'} = obj
console.log(address) // 龙华
// 是给事先声明的变量赋值,则赋值表达式必须包含在一对括号中
let name, age;
({name,age}) = obj
null和 undefined 不能被解构
- 嵌套解构
解构对于引用嵌套的属性或赋值目标没有限制。为此,可以通过解构来复制对象属性
javascript
let person = {
name: '张三',
age: 27,
job: {
title: '法外狂徒'
}
};
let personCopy = {}
({name:personCopy.name,age:personCopy.age,} = person)
// 因为一个对象的引用被赋值给 personCopy,所以修改person.job 对象的属性也会影响 personCopy
person.job.title = '罗老师'
console.log(person) //{name: '张三',age: 27,job: {title: '罗老师'}}
console.log(personCopy) //{name: '张三',age: 27,job: {title: '罗老师'}}
// 解构赋值可以使用嵌套结构,以匹配嵌套的属性
const {obj:{title}} = person
console.log(title) //罗老师
// 在外层属性没有定义的情况下不能使用嵌套解构。无论源对象还是目标对象都一样
let person = {
job: {
title: 'Software engineer'
}
};
let personCopy = {};
// foo 在源对象上是 undefined
({
foo: {
bar: personCopy.bar
}
} = person);
// TypeError: Cannot destructure property 'bar' of 'undefined' or 'null'.
// job 在目标对象上是 undefined
({
job: {
title: personCopy.job.title
}
} = person);
// TypeError: Cannot set property 'title' of undefined
- 部分解构
如果一个解构表达式涉及 多个赋值,开始的赋值成功而后面的赋值出错,则整个解构赋值只会完成一部分
javascript
let person = {
name: 'Matt',
age: 27
};
let personName, personBar, personAge;
try {
// person.foo 是 undefined,因此会抛出错误
({name: personName, foo: { bar: personBar }, age: personAge} = person);
} catch(e) {}
console.log(personName, personBar, personAge);
// Matt, undefined, undefined
- 参数上下文匹配
在函数参数列表中也可以进行解构赋值。对参数的解构赋值不会影响 arguments 对象,但可以在函数签名中声明在函数体内使用局部变量
javascript
let person = {
name: 'Matt',
age: 27
};
function printPerson(foo, {name, age}, bar) {
console.log(arguments); // 123 { name: 'Matt', age: 27 } abc
console.log(name, age); //Matt 27
}
printPerson('123',person,'abc')
创建对象
虽然使用 Object 构造函数或对象字面量可以方便地创建对象,但这些方式也有明显不足:创建具有同样接口的多个对象需要重复编写很多代码。我们可以使用一种称为面向对象编程的编程范式来解决这个问题。
工厂模式
工厂模式是一种创建型设计模式,通过工厂函数来封装对象
的创建过程,避免在代码中直接使用 new
关键字创建对象。工厂模式简化了对象的创建过程,降低了代码的耦合性,提高了代码的可维护性和可读性。工厂模式可以返回不同类型的对象,具体的实现细节可以隐藏在工厂函数的内部,使得代码更加模块化。另外,通过工厂模式,我们可以在代码中使用抽象的接口而不是具体的对象类型,从而在代码重构时变得更加灵活
javascript
function createPerson(name, age, job) {
let o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function() {
console.log(this.name);
};
return o;
}
let person1 = createPerson("张三", 29, "法外狂徒");
let person2 = createPerson("孙悟空", 2700, "弼马温")
函数 createPerson()接收 3 个参数,根据这几个参数构建了一个包含 Person 信息的对象。 可以用不同的参数多次调用这个函数,每次都会返回包含3个属性和1个方法的对象
工厂模式虽然可以解决创建多个类似对象的问题,但没有解决对象标识问题(不能保证对象之间的唯一性)
构造函数模式
构造函数是用于创建特定类型对象的。Object
和Array
是原生的构造函数, 运行时可以直接在执行环境中使用。我们也可以自定义构造函数,以函数的形式为自己的对象类型定义属性和方法。
javascript
function Person(name, age, job){
this.name = name
this.age = age
this.job = job
this.say=function(){
console.log('hellow,world')
}
}
let person1 = new Person("张三", 29, "法外狂徒")
let person2 = new Person("孙悟空", 2700, "弼马温")
person1.say(); // 张三
person2.say(); // 孙悟空
console.log(person1.constructor == Person); // true
console.log(person2.constructor == Person); // true
在这个例子中,Person函数
取代了工厂函数中的createPerson函数
,实际上Person函数内部
和createPerson函数
基本是相同的,只有以下几点不同
- 没有显式地创建对象。
- 属性和方法直接赋值给了 this。
- 没有 return。
constructor
是用于标识对象类型的,不过我们一般使用 instanceof
操作符来确定对象类型。
定义自定义构造函数可以确保实例被标识为特定类型,相比于工厂模式,这是一个很大的好处。
注意:构造函数首字母大写
要创建 Person 的实例,应使用 new 操作符。 以这种方式调用构造函数会执行如下操作
- 在内存中创建一个新对象。
- 这个新对象内部的[[Prototype]]特性被赋值为构造函数的 prototype 属性。
- 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)。
- 执行构造函数内部的代码(给新对象添加属性)。
- 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象
javascript
// 手写new
function new(fun,...args){
// 创建一个新对象,并将其 __proto__ 属性设置为构造函数的原型对象
ler newObj = Object.create(fun.prototype)
// 将构造函数的上下文绑定到新创建的对象上,并执行构造函数
const result = fun.apply(fun,newObj)
// 返回新创建的对象,如果构造函数显式返回了一个对象,则返回该对象,否则返回新对象
return result instanceof Object ? result : obj;
}
- 构造函数也是函数
构造函数与普通函数唯一的区别就是调用方式不同。除此之外,构造函数也是函数。并没有把某个 函数定义为构造函数的特殊语法。任何函数只要使用 new 操作符调用就是构造函数,而不使用 new 操作符调用的函数就是普通函数。像上面的Person()函数可以调用就是一个普通函数,使用new 调用就是构造函数
javascript
// 普通函数
Person("张三", 29, "法外狂徒")
window.say() // 张三
// 构造函数
let person = new Person("张三", 29, "法外狂徒")
person.say() // 张三
// 在另一个对象的作用域中调用
let o = new Object();
Person.call(o, "张三", 29, "法外狂徒");
o.say(); // 张三
在调用一个函数而没有明确设置this值的情况下(即没有作为对象的方法调用,或者没有使用 call()/apply()调用),this始终指向Global对象
(浏览器里就是window对象),所以普通函数调用时,window对象上有了一个say()方法;在另一个函数的作用域调用的情况下,使用了call()
(或 apply())方法,会改变this的指向,这里的this指向了o对象,所以所有属性和 say()方法都会添加到对象 o 上面。
原型模式
每个函数都会创建一个prototype
属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。实际上,这个对象就是通过调用构造函数创建的对象的原型。使用原型对象的好处是,在它上面定义的属性和方法可以被对象实例共享。
javascript
function Person(){}
Person.prototype.name = '张三'
Person.prototype.age = 18
Person.prototype.say=function(){
console.log(this.name)
}
let p1 = new Person()
let p2 = new Person()
p1.say() //张三
p2.say() //张三
console.log(p1.say==p2.say) //true
这里的属性和方法都添加到了prototype
上,构造函数体中什么都没有。但这样定义之后,调用构造函数创建的新对象仍然拥有相应的属性和方法。与构造函数模式不同,使用这种原型模式定义的属性和方法是由所有实例共享的。因此 p1 和 p2 访问的都是相同的属性和相同的 say()函数
- 理解原型
只要创建一个函数就会为这个函数创建一个prototype
属性(指向原型对象),默认情况下,所有的原型对象都有一个constructor
指回构造函数,例如 Person.prototype.constructor 指向Proson。
在自定义构造函数时,原型对象默认只会获得constructor
属性,其他的方法都继承Object。每次调用构造函数都会创建一个实例,这个实例的内部Prototype
指针会指向被赋值为构造函数的原型对象。我们可以通过__proto__
属性访问对象的原型。
javascript
console.log(p1.__proto__ == Person.prototype) //true
console.log(p1.__proto__.__proto__.constructor==Object) //true
console.log(p1.__proto__.__proto__.__proto__==null) //true
** 正常的原型链都会终止于 Object 的原型对象。 Object 原型的原型是 null**
Person构造函数、Person原型对象、Person实例之间的关系。
Person构造函数: Person.prototype指向原型对象,Person.prototype.contructor指回构造函数。
实例对象 p1.__proto__指回Person.prototype
不是所有的实现都对外暴露了[[Peototype]],所以js给我们提供了isPrototypeOf()
方法来测试一个对象是否为另一个对象的原型
javascript
console.log(Person.prototype.isPrototypeOf(p1)) // true
console.log(Object.prototype.isPrototypeOf(Person)) // true 所有 JavaScript 对象都继承自 Object.prototype
在ES5中,我们可以使用Object.getPrototypeOf()
来获取原型对象
javascript
console.log(Object.getPrototypeOf(p1) == Person.prototype)
在ES6中,我们可以使用Object.setPrototypeOf()
来设置原型对象
javascript
const Person = {
name:'',
age:18,
say(){
console.log(`我是${this.name}`)
}
}
let p = {
name:'张三'
}
Object.setPrototypeOf(p,Person) //将 Person 设置为 p 的原型对象
console.log(p.say())
不过 Object.setPrototypeOf()
方法可能会造成较大的性能影响,我们一般使用Object.create()来创建新对象,并为其指定原型
javascript
const Person = {
name:'',
age:18,
say(){
console.log(`我是${this.name}`)
}
}
let p = Object.create(Person)
p.name = '张三'
console.log(p.say())
- 原型层级
在通过对象访问属性时,会按照这个属性的名称开始搜索,先搜索对象实例本身,如果有就返回,没有就去原型对象上找,如果有就返回,没有就一直找到Object,还找不到就返回null。
如果在原型对象和实例对象上都有相同的属性,实例对象的属性会遮住原型对象的属性,优先返回实例对象的值
可以使用hasOwnProperty()
方法确定属性是在实例上还是原型对象上。属性存在于调用它的对象实例上时返回true。
javascript
function Person(){}
Person.prototype.name = '张三'
Person.prototype.age = 18
Person.prototype.say = function(){
console.log(this.name)
}
let p1 = new Person()
console.log(p1.name) //张三 来自原型对象
console.log(p1.hasOwnProperty('name')) //false
console.log(Person.hasOwnProperty('name')) //true
p1.name = '法外狂徒'
console.log(p1.name) // 法外狂徒 来自实例对象
console.log(p1.hasOwnProperty('name')) //true
console.log(Person.hasOwnProperty('name')) //true
- 原型和in操作符
有两种方式可以使用in
操作符:单独使用和在for-in
循坏中使用。
3.1 单独使用
in
操作符会在可以通过对象访问指定属性时返回true,不管是在实例上还是原型上
javascript
function Person(){}
Person.prototype.name = '张三'
Person.prototype.age = 18
Person.prototype.say = function(){
console.log(this.name)
}
let p1 = new Person()
console.log('name' in p1) //true
console.log('name' in Person) //true
判断一个值是否存在原型上,可以通过in
和hasOwnproperty()
来判断
javascript
function hasPrototypeProperty(object, name){
return !object.hasOwnProperty(name) && (name in object);
}
只要可以通过对象方法,in
操作符就返回true,而hasOwnproperty()
只有属性存在实例上时才返回true。所以当in
返回true且hasOwnproperty()
返回false时说明该属性是一个原型属性 。
3.2 for-in循环使用
在 for-in
循环中使用 in 操作符时,可以通过对象访问且可以被枚举的属性都会返回,包括实例属性和原型属性。
要获得对象上所有可枚举的实例属性,可以使用Object.keys()
方法。这个方法接收一个对象作 为参数,返回包含该对象所有可枚举属性名称的字符串数组。
javascript
function Person() {}
Person.prototype.name = "张三";
Person.prototype.age = 29;
Person.prototype.job = "法外狂徒";
Person.prototype.sayName = function() {
console.log(this.name);
};
let keys = Object.keys(Person.prototype);
console.log(keys); // "[name,age,job,sayName]"
let p1 = new Person();
p1.name = "李四";
p1.age = 31;
let p1keys = Object.keys(p1);
console.log(p1keys); // "[name,age]"
如果想列出所有实例属性,无论是否可以枚举,都可以使用 Object.getOwnPropertyNames()
javascript
let keys = Object.getOwnPropertyNames(Person.prototype);
console.log(keys); // "[constructor,name,age,job,sayName]"
constructor
是一个不可枚举的属性。
- 属性枚举顺序
for-in 循环和 Object.keys() 的枚举顺序是不确定的 ,取决于 JavaScript 引擎,可能因浏览器而异 。
Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()和 Object.assign() 的枚举顺序是确定性的。先以升序枚举数值键,然后以插入顺序枚举字符串和符号键
继承
继承是面向对象编程中讨论最多的话题。很多面向对象语言都支持两种继承:接口继承和实现继承。 前者只继承方法签名,后者继承实际的方法。接口继承在 ECMAScript 中是不可能的,因为函数没有签 名。实现继承是 ECMAScript 唯一支持的继承方式,而这主要是通过原型链实现的。
原型链
原型链继承
javascript
function Parent() {
this.name = 'Parent';
}
Parent.prototype.sayHello = function() {
console.log('Hello, ' + this.name);
};
function Child() {
this.name = 'Child';
}
Child.prototype = new Parent(); // 将父对象的实例指定为子对象的原型
var child = new Child();
child.sayHello(); // 输出 'Hello, Child'