JS创建对象
创建对象,主要有工厂模式、构造函数模式、原型模式三种。本文主要分析了这三种模式的特点、利弊,以及一些细节问题。
参考:js高程 红宝书(第四版)
一、工厂模式
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");
复制代码
- 优点:创建多个相似对象的问题
- 缺点:没有解决对象识别问题(即新创建的对象是什么类型?)
二、构造函数模式
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
复制代码
1. 构造函数模式 与 工厂模式 是比较像的,只是有如下区别:
- 没有显式地创建对象
- 属性和方法直接赋值给了 this
- 没有 return
- 函数名 Person 的首字母是大写
2. 用 new 调用构造函数会执行如下操作:
- 在内存中创建一个新对象。
- 这个新对象内部的 [[Prototype]]_ 特性被赋值为构造函数的 prototype 属性。
- 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)。
- 执行构造函数内部的代码(给新对象添加属性)。
- 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。
3. 构造函数的问题:
- 其定义的方法在每个实例上都创建一遍,不同实例的方法不是同一个 Function 实例(JS 中函数是对象,因此每次定义函数时,都会初始化一个对象。)
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = new Function("console.log(this.name)"); // 逻辑等价
}
复制代码
要解决这个问题,可以把函数定义 转移到 构造函数外部:
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName() {
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
复制代码
构造函数内 sayName 属性中包含的只是一个指向外部全局函数的指针,所以 person1 和 person2共享了定义在全局作用域上的 sayName()函数。但是这样处理也是有弊端的:
- 虽解决了相同逻辑的函数重复定义问题,但是全局作用域被搞乱了。若这个对象需要多个函数,就需要在全局作用域中定义多个函数。
- 该问题可用原型模式来解决。
三、原型模式
每个函数都会创建一个 prototype 属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。
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();
person1.sayName(); // "Nicholas"
let person2 = new Person();
person2.sayName(); // "Nicholas"
console.log(person1.sayName == person2.sayName); // true
复制代码
原型相关的方法总结:
1. isPrototypeOf():确定 实例 和 原型对象 的关系;
console.log(Person.prototype.isPrototypeOf(person1)); // true
console.log(Person.prototype.isPrototypeOf(person2)); // true
复制代码
2. Object.getPrototypeOf():返回参数的内部特性[[Prototype]]的值。
console.log(Object.getPrototypeOf(person1) == Person.prototype); // true
console.log(Object.getPrototypeOf(person1).name); // "Nicholas"
复制代码
3. Object.setPrototypeOf():向实例的私有特性[[Prototype]]写入一个新值。会严重影响代码性能,不推荐!可以使用Object.create()代替
4. Object.create():创建新对象,并为其指定原型对象
let biped = {
numLegs: 2
};
let person = Object.create(biped);
person.name = 'Matt';
console.log(person.name); // Matt
console.log(person.numLegs); // 2,访问原型上的属性
console.log(Object.getPrototypeOf(person) === biped); // true
复制代码
5. hasOwnProperty()方法用于确定某个属性 是在实例上 还是在原型对象上。属性存在于调用它的对象实例上时返回 true
console.log(person1.hasOwnProperty("name")); // false
person1.name = "Greg";
console.log(person1.name); // "Greg",来自实例
console.log(person1.hasOwnProperty("name")); // true
复制代码
6. 原型和 in 操作符
in 操作符有两种使用方式:单独使用和在 for-in 循环中使用。
- 在单独使用时,in 操作符会在可以通过对象访问指定属性时返回 true,无论该属性是在实例上还是在原型上。
如果要确定某个属性 是否存在于原型上,则可以像下面这样同时使用 hasOwnProperty()和 in 操作符:
function hasPrototypeProperty(object, name){
return !object.hasOwnProperty(name) && (name in object);
}
复制代码
- for-in 循环中使用 in 操作符时,可以通过对象访问且可以被枚举的属性都会返回,包括实例属性和原型属性。
7. Object.keys():获得对象上所有可枚举的实例属性,返回一个字符串数组。
8. Object.getOwnPropertyNames():列出所有实例属性,无论是否可以枚举。
let keys = Object.getOwnPropertyNames(Person.prototype);
console.log(keys); // "[constructor,name,age,job,sayName]"
复制代码
注意,返回的结果中包含了一个不可枚举的属性 constructor。
四、对象迭代
这两个静态方法Object.values()和 Object.entries()接收一个对象,返回它们内容的数组。
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
复制代码
别急,稍作修改即可:
function Person() {}
Person.prototype = {
// constructor: Person, 用这种方式恢复 constructor 属性会创建一个[[Enumerable]]为 true 的属性。而原生 constructor 属性默认是不可枚举的
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName() {
console.log(this.name);
}
};
// 恢复 constructor 属性
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person
});
复制代码
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(); // 错误
复制代码
实例的 [[Prototype]] 指针是在调用构造函数时自动赋值的,指向最初创建的原型。(原型被重写后,便是另一个原型对象了,会切断最初原型与构造函数的联系)
3. 原型的问题
(1)原型主要有这两个问题:
- 弱化了向构造函数传递初始化参数的能力,会导致所有实例默认都取得相同的属性值。
- 原型的最主要问题源自它的共享特性,原型上的所有属性是在实例间共享的。
所以实际开发中通常 不单独 使用原型模式 。
(2)原型共享特性 适用场景分析:
- 对函数来说比较合适;
- 对于包含原始值的属性也还好,可以在实例上添加同名属性来简单地遮蔽原型上的属性;
- 对于包含引用值的属性就很糟糕了
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"); // friends属性是原型上的,该属性也会在其他实例上体现出来
console.log(person1.friends); // "Shelby,Court,Van"
console.log(person2.friends); // "Shelby,Court,Van"
console.log(person1.friends === person2.friends); // true
复制代码