目录
1. 理解对象
Object是 JavaScript 的一种数据类型,它用于存储各种键值集合和更复杂的实体。可以通过 object()构造函数或者使用对象字面量的方式创建对象。
// 通过构造函数创建
let person = new Object();
// 通过字面量创建
let person = {};
1.1 属性的类型
属性分两种:数据属性和访问器属性.
1.1.1 数据属型
数据属性包含一个保存数据值的位置,值在这个位置读取/写入,有4个特性描述它们的行为。
1.[[Configurable]]: 属性是否可配置,默认为true,表示属性可以通过delete删除并重新定义,可修改及改为访问器属性。
2.[[Enumerable]]:属性是否可枚举,默认为true,表示属性可以通过for-in循环返回。
3.[[Writable]]:属性的值是否可修改,默认为true,表示属性的值可修改。
4.[[Value]]:属性的值,默认为undefined, 为读取和写入属性值的位置。
可以通过Object.defineProperty()来修改属性的默认特性。该方法接受三个参数:要给其添加属性对象,属性的名称和特性对象(4个特性)
在严格模式下,尝试修改只读属性的值和删除不可配置的属性值会抛出错误。
let person = {};
Object.defineProperty(person, "name", {
configurable: true,
enumerable: true,
writable: true,
value: "JIE"
})
可以对同一个属性多次调用Object.defineProperty(),但在把configurable设置为false之后就会受限制了。
let person = {};
Object.defineProperty(person, "name", {
configurable: false,
value: "JIE"
})
// 抛出错误
Object.defineProperty(person, "name", {
configurable: true,
value: "JIE"
})
注: 在调用Object.defineProperty()时,configurable、enumerable和writable的值如果不指定,则都默认为false
1.1.2 访问器属性
访问器属性不包含数据值,包含一个获取(getter)函数和一个设置(setter)函数。
访问器有4个特性
1.[[Configurable]]: 属性是否可配置,默认为true,表示属性可以通过delete删除并重新定义,可修改及改为访问器属性。
2.[[Enumerable]]:属性是否可枚举,默认为true,表示属性可以通过for-in循环返回。
3. [[Get]]:获取函数,在读取属性时调用,默认值为undefined。
4. [[Set]]:设置函数,在写入属性时调用,默认值为undefined。
访问器不能直接定义,必须使用Object.defineProperty()。
在严格模式下,尝试写入只定义了获取函数(意味着属性为只读)的属性会抛出错误。类似地,只有一个设置函数的属性是不能读取的,非严格模式下读取会返回undefined,严格模式下会抛出错误。
let person = {
name: 'JIE',
edition: 1
};
Object.defineProperty(person, "name1", {
get() {
return this.name;
},
set(newValue) {
this.name = newValue;
this.edition ++;
}
});
person.name1 = 'jack';
console.log(person.edition); // 2
1.2 定义多个属性
Object.define-Properties()方法
let person = {age_:20};
Object.defineProperties(person, {
name: {
value: 'JIE'
},
age: {
get () {
return this.age_
}
}
}
);
console.log(person.age);
1.3 读取属性的特性
Object.getOwnPropertyDescriptor()方法
// 接上段代码
let descriptor = Object.getOwnPropertyDescriptor(person, "name");
console.log(descriptor.value);
console.log(descriptor.configurable); // false
console.log(descriptor.enumerable); // false
console.log(descriptor.writable); // false
Object.getOwnPropertyDescriptors()方法
let person = {age_:20};
Object.defineProperties(person, {
name: {
value: 'JIE'
},
age: {
get () {
return this.age_
}
}
}
);
let descriptors = Object.getOwnPropertyDescriptors(person);
console.log(JSON.stringify(descriptors));
// {
// age_: { value: 20, writable: true, enumerable: true, configurable: true },
// name: {
// value: "JIE",
// writable: false,
// enumerable: false,
// configurable: false,
// },
// age: { enumerable: false, configurable: false },
// };
1.4 合并对象
Object.assign()方法
Object.assign()实际上对每个源对象执行的是浅复制。如果多个源对象都有相同的属性,则使用最后一个复制的值。
let targetObj,sourceObj,result;
targetObj= {};
sourceObj = {name:'JIE'};
result = Object.assign(targetObj,sourceObj);
// Object.assign修改目标对象
// 也会返回修改后的目标对象
console.log(targetObj === result); // true
console.log(targetObj !== sourceObj); // true
console.log(result); // {name:'JIE'}
console.log(targetObj); // {name:'JIE'}
2.创建对象
虽然使用Object构造函数或对象字面量可以方便地创建对象,但这些方式也有明显不足:创建多个对象时需要重复编写相同代码。
2.1 工厂模式
工厂模式是一种众所周知的设计模式,广泛应用于软件工程领域,用于抽象创建特定对象的过程。
这种工厂模式虽然可以解决创建多个类似对象的问题,但没有解决对象标识问题(即新创建的对象是什么类型)。
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("Nicholas", 29, "Software Engineer");
let person2 = createPerson("Greg", 27, "Doctor");
2.2构造函数模式
构造函数用于创建特定类型对象;
任何函数只要使用new操作符调用就是构造函数,而不使用new操作符调用的函数就是普通函数;
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
console.log(this.name);
};
}
let person1 = new Person("Nicholas", 29, "Software Engineer");
let person2 = new Person("Greg", 27, "Doctor");
person1.sayName(); // Nicholas
person2.sayName(); // Greg
构造函数的问题
会在每个实例上都创建一遍
console.log(person1.sayName == person2.sayName); // false
因为都是做一样的事,所以没必要定义两个不同的Function实例,可以通过原型模式来解决。
2.3 原型模式
每个函数都会创建一个prototype属性,这个属性是一个对象,是通过调用构造函数创建的对象的原型。使用原型对象的好处是,属性和方法可以被对象实例共享。
2.3.1 理解原型
默认情况下,所有原型对象自动获得一个名为constructor的属性,指回与之关联的构造函数。
通过代码理解
/**
* 构造函数可以是函数表达式
* 也可以是函数声明,因此以下两种形式都可以:
* function Person() {}
* let Person = function() {}
*/
function Person() {}
/**
* 声明之后,构造函数就有了一个
* 与之关联的原型对象:
*/
console.log(typeof Person.prototype);
console.log(Person.prototype);
// {
// constructor: f Person(),
// __proto__: Object
// }
/**
* 如前所述,构造函数有一个prototype属性
* 引用其原型对象,而这个原型对象也有一个
* constructor属性,引用这个构造函数
* 换句话说,两者循环引用:
*/
console.log(Person.prototype.constructor === Person); // true
/**
* 正常的原型链都会终止于Object的原型对象
* Object原型的原型是null
*/
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Person.prototype.__proto__.constructor === Object); // true
console.log(Person.prototype.__proto__.__proto__ === null); // true
console.log(Person.prototype.__proto__);
// {
// constructor: f Object(),
// toString: ...
// hasOwnProperty: ...
// isPrototypeOf: ...
// ...
// }
let person1 = new Person(),
person2 = new Person();
/**
* 构造函数、原型对象和实例
* 是3 个完全不同的对象:
*/
console.log(person1 ! == Person); // true
console.log(person1 ! == Person.prototype); // true
console.log(Person.prototype ! == Person); // true
/**
* 实例通过__proto__链接到原型对象,
* 它实际上指向隐藏特性[[Prototype]]
*
* 构造函数通过prototype属性链接到原型对象
*
* 实例与构造函数没有直接联系,与原型对象有直接联系
*/
console.log(person1.__proto__ === Person.prototype); // true
conosle.log(person1.__proto__.constructor === Person); // true
/**
* 同一个构造函数创建的两个实例
* 共享同一个原型对象:
*/
console.log(person1.__proto__ === person2.__proto__); // true
/**
* instanceof检查实例的原型链中
* 是否包含指定构造函数的原型:
*/
console.log(person1 instanceof Person); // true
console.log(person1 instanceof Object); // true
console.log(Person.prototype instanceof Object); // true
对象关系图
2.3.2 原型层级
hasOwnProperty()判断属性是否在实例上
function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
console.log(this.name);
};
let person1 = new Person();
let person2 = new Person();
console.log(person1.hasOwnProperty("name")); // false
person1.name = "Greg";
console.log(person1.name); // "Greg",来自实例
console.log(person1.hasOwnProperty("name")); // true
console.log(person2.name); // "Nicholas",来自原型
console.log(person2.hasOwnProperty("name")); // false
delete person1.name;
console.log(person1.name); // "Nicholas",来自原型
console.log(person1.hasOwnProperty("name")); // false
图示
2.3.3 原型和in操作符
无论属性在实例上还是在原型上都返回true
"name" in person
如果要确定某个属性是否存在于原型上,则可以像下面这样同时使用hasOwnProperty()和in操作符:
function hasPrototypeProperty(object, name){
return ! object.hasOwnProperty(name) && (name in object);
}
3. 对象迭代
ES7新增:Object.values()返回对象值的数组,Object.entries()返回键/值对的数组。
const o = {
foo: 'bar',
baz: 1,
qux: {}
};
console.log(Object.values(o));
// ["bar", 1, {}]
console.log(Object.entries((o)));
// [["foo", "bar"], ["baz", 1], ["qux", {}]]
3.1其他原型语法
为了减少代码冗余,也为了从视觉上更好地封装原型功能,直接通过一个包含所有属性和方法的对象字面量来重写原型成为了一种常见的做法,如下面的例子所示:
function Person() {}
Person.prototype = {
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName() {
console.log(this.name);
}
};
这样重写之后,Person.prototype的constructor属性就不指向Person了;
在创建函数时,也会创建它的prototype对象,同时会自动给这个原型的constructor属性赋值;
而上面的写法完全重写了默认的prototype对象,因此其constructor属性也指向了完全不同的新对象(Object构造函数),不再指向原来的构造函数;
// 接上段代码
let friend = new Person();
console.log(friend instanceof Object); // true
console.log(friend instanceof Person); // true
console.log(friend.constructor == Person); // false
console.log(friend.constructor == Object); // true
如果constructor的值很重要,则可以像下面这样在重写原型对象时专门设置一下它的值:
function Person() {
}
Person.prototype = {
constructor: Person,
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName() {
console.log(this.name);
}
};
但要注意,以这种方式恢复constructor属性会创建一个[[Enumerable]]为true的属性。而原生constructor属性默认是不可枚举的。可以使用Object.defineProperty()方法来定义constructor属性:
function Person() {}
Person.prototype = {
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName() {
console.log(this.name);
}
};
// 恢复constructor属性
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person
});
3.2 原型的动态性
对原型对象所做的修改会在实例上反映出来
let friend = new Person();
Person.prototype.sayHi = function() {
console.log("hi");
};
friend.sayHi(); // "hi"
重写整个原型会切断最初原型与构造函数的联系,但实例引用的仍然是最初的原型。
实例只有指向原型的指针,没有指向构造函数的指针。来看下面的例子:
function Person() {}
let friend = new Person();
Person.prototype = {
constructor: Person,
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName() {
console.log(this.name);
}
};
friend.sayName(); // 错误
在这个例子中,Person的新实例是在重写原型对象之前创建的。在调用friend.sayName()的时候,会导致错误。这是因为firend指向的原型还是最初的原型,而这个原型上并没有sayName属性。
3.3 原生对象原型
所有原生引用类型的构造函数(包括Object、Array、String等)都在原型上定义了实例方法;
比如,数组实例的sort()方法就是Array.prototype上定义的;
console.log(typeof Array.prototype.sort); // "function"
通过原生对象的原型可以取得所有默认方法的引用,也可以给原生类型的实例定义新的方法;
给String原始值包装类型的实例添加了一个startsWith()方法:
String.prototype.startsWith = function (text) {
return this.indexOf(text) === 0;
};
let msg = "Hello world! ";
console.log(msg.startsWith("Hello")); // true
注:不推荐在产品环境中修改原生对象原型,可能引发命名冲突和重写原生方法。推荐的做法是创建一个自定义的类,继承原生类型。
3.4 原型的问题
弱化了向构造函数传递初始化参数的能力;
导致所有实例默认都取得相同的属性值;
主要问题源自它的共享特性(修改引用类型),代码如下:
function Person() {}
Person.prototype = {
constructor: Person,
name: "Nicholas",
age: 29,
job: "Software Engineer",
friends: ["Shelby", "Court"],
sayName() {
console.log(this.name);
}
};
let person1 = new Person();
let person2 = new Person();
person1.friends.push("Van");
console.log(person1.friends); // "Shelby,Court,Van"
console.log(person2.friends); // "Shelby,Court,Van"
console.log(person1.friends === person2.friends); // true
不同的实例应该有属于自己的属性副本。这就是实际开发中通常不单独使用原型模式的原因。