JavaScript中创建对象的多种方式和优缺点

2023-06-10 14:29:39 浏览数 (1)

前言

ES5.1 并没有正式支持面向对象的结构,比如类的继承。但是我们可以通过原型来模拟。 从ES6 开始支持了类和继承,但其实只是封装了 ES5.1 的构造函数和原型继承的语法糖而已。

工厂模式

代码语言: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("andy", 18, "Software Engineer");
let person2 = createPerson("KangKang", 27, "Doctor");
person1.sayName(); // andy
person2.sayName(); // KangKang

这种方式创建的对象,不知道是什么类型的,所以没办法标识。

构造函数模式

es中 像 ObjectArray 这样的原生构造函数,可以直接在运行环境中执行。而我们也可以自定义构造函数,通过这个构造函数给对象类型定义属性和方法。例如:

代码语言:javascript复制
function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = function() {
    console.log(this.name)
  }
}
let person1 = createPerson("andy", 18, "Software Engineer");
let person2 = createPerson("KangKang", 27, "Doctor");
person1.sayName(); // andy
person2.sayName(); // KangKang

这种方式和工厂函数创建对象的区别:

  • 在这个例子中,没有显示的创建对象。
  • 属性和方法直接赋值给了this。
  • 没有 return。

优缺点

优点:

自定义构造函数可以确保实例被标识为特定类型,相比于工厂模式,这是一个很大的好处

缺点:

主要问题在于,其定义的方法会在每个实例上都创建一遍。

new 操作符

要创建 Person 的实例,需使用 new 操作符。用 new 操作符创建实例大约会执行一下几个步骤:

  1. 在内存中插件一个新对象
  2. 新对象内部的 [[Prototype]] 特性被赋值为构造函数的 Prototype 属性。
  3. 构造函数内部的 this 被赋值给新对象(this 指向新对象)
  4. 执行构造函数(给新对象添加属性)
  5. 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。

原型模式

我们通过原型模式创建对象,

代码语言:javascript复制
function Person() {}
Person.prototype.name = "Andy";
Person.prototype.age = 24;
Person.prototype.sayName = function() {
  console.log(this.name)
}

let person = new Person();	// person.sayName();
person.sayName();	// "Andy"

在调用 person.sayName() 时,会发生两步搜索。首先,在 person 实例上查找是否存在 sayName 属性/方法,如果没有就继续从 person 的原型上(person.__proto__Person.prototype)找是否存在 sayName 属性/方法,存在则返回这个函数。这就是原型用于在多个对象实例间共享属性和方法的原理。

上面的例子中每次添加方法或者属性都要写一遍 Person.prototype,比较麻烦且视觉上不舒服,我们可以通过对象字面量创建的新对象赋值给 Person.prototype

代码语言:javascript复制
function Person() {
}

Person.prototype = {
    name:"Andy",
    age: 24
}

console.log(Person.prototype.constructor == Person)	// false
console.log(Person.prototype.constructor == Object)	// false

在这个例子中, Person.prototype 被设置为等于一个通过对象字面量创建的新对象,这样重写原型之后 Person.prototypeconstructor 属性就不再指向 Person 了。

但是从原型上搜索值的过程是动态的,所以就算实例在修改原型之前就已经存在,任何时候对原型对象所做的修改,在实例上也会存在这个修改,看例子:

代码语言:javascript复制
let teacher = new Person();

Person.prototype.sayHi = function() {
  console.log("hi");
}

teacher.sayHi();	// "hi"

注意:虽然随时能给原型添加属性和方法,并且在所有对象实例上也跟着反应。但这种跟重写整个原型是两回事。实例的[[Prototype]] 指针是在调用构造函数时自动赋值的,所以就算把原型改成不同的对象,这个指针也不会变,实例只有指向原型的指针,没有指向构造函数的指针。例子:

代码语言:javascript复制
function Person() {}
let friend = new Person();
Person.prototype = {
  constructor: Person,
  name: "Andy",
  age: 24,
  sayName() {
    console.log(this.name);
  }
};
friend.sayName(); // 错误

虽然可以通过实例读取原型对象上的值,但不可能通过实例重写这些值。例子:

代码语言:javascript复制
function Person() {}

Person.prototype.name = "KangKang";

let person1 = new Person();
let person2 = new Person();

person1.name = "Andy";
console.log(person1.name); // "Andy",来自实例

console.log(person2.name); // "KangKang",来自原型

不过,使用 delete 操作符可以完全删除实例上的这个属性,从而让标识符解析过程能够继续搜索 原型对象

代码语言:javascript复制
function Person() {}

Person.prototype.name = "KangKang";

let person1 = new Person();
let person2 = new Person();

person1.name = "Andy";
console.log(person1.name); // "Andy",来自实例

console.log(person2.name); // "KangKang",来自原型

delete person1.name;
console.log(person1.name); // "KangKang"

通过属性在实例上还是原型对象上

看了 mdn上 hasOwnProperty() 的解释:该方法返回一个布尔值,表示对象自有属性(而不是继承来的属性)中是否具有指定的属性。所以我们可以用 hasOwnProperty()方法来确定某个属性是在这个实例上还是在原型对象上。如下例子:

代码语言:javascript复制
function Person() {}

Person.prototype.name = "KangKang";

let person1 = new Person();
let person2 = new Person();

console.log(person1.hasOwnProperty('name'))	// false

person1.name = "Andy"	
console.log(person1.name)	// "andy" 来自实例
console.log(person1.hasOwnProperty('name'))	// true

console.log(person2.name)	// "KangKang" 来自原型
console.log(person2.hasOwnProperty('name'))	// false

// 结合 delete
delete person1.name;
console.log(person1.name)	// "KangKang" 来自原型
console.log(person1.hasOwnProperty('name'))	// false

原型模式的问题

会导致所有实例默认都取得相同的属性。

共享特性会导致只要修改某个相同原型模式创建的实例的引用类型数据,其他所有实例的该属性都会跟着改变

代码语言:javascript复制
function Person() {}

Person.prototype = {
  constructor: Person,
  name: "Andy",
  friends: ["KangKang", "Juli"],
  sayName() {
    console.log(this.name);
  }
};
let person1 = new Person();
let person2 = new Person();
person1.friends.push("Jack Cheng");
console.log(person1.friends); // "KangKang,Juli,Jack Cheng"
console.log(person2.friends); // "KangKang,Juli,Jack Cheng"
console.log(person1.friends === person2.friends); // true

原型和 in 操作符

in操作符在单独使用时,可以判断对象能否访问某属性,能则返回 true,无论该属性是在实例上还是在原型上都会访问 true

代码语言:javascript复制
function Person() {}

Person.prototype.name = "KangKang";

let person1 = new Person();
let person2 = new Person();

person1.name = "Andy";
console.log(person1.name); // "Andy",来自实例
console.log('name' in person1)	// true

console.log(person2.name); // "KangKang",来自原型
console.log('name' in person2)	//true

delete person1.name;
console.log(person1.name); // "KangKang"
console.log('name' in person1)	// true

通过inhasOwnProperty来判断某个属性是否存在于原型上:

代码语言:javascript复制
function hasPrototypeProperty(object, name) {
    return !object.hasOwnProperty(name) && (name in object);
}

// 测试方法
function Person() {}
Person.prototype.name = "KangKang";
Person.prototype.age = 29;

let person = new Person();
console.log(person.hasPrototypeProperty(person, 'name'))	// true

person.name = "Andy"
console.log(person.hasPrototypeProperty(person, 'name'))	//false

一些遍历实例属性的方法和操作符

  • for-in 循环中的 in 操作符

可以通过对象访问且可以被枚举的属性都会返回,包括实例属性和原型属性。遮蔽原型中不可枚举( [[Enumerable]] 特性被设置为 false )属性的实例属性也会 在 for-in 循环中返回,因为默认情况下开发者定义的属性都是可枚举的。

  • Object.keys() 返回所有可枚举实例属性
  • Object.getOwnPropertyNames() 返回所有实例属性,无论是否可枚举。
  • Object.getOwnPropertySymbols() 这个方法与Object.getOwnPropertyNames()类似,只是针对已符号为键的属性的实例对象

相关资料

  • 《JavaScript高级程序设计》(第四版)

推荐笔记: github 上总结的:https://github.com/mqyqingfeng/Blog/issues/15

写在最后

我是 AndyHu,目前暂时是一枚前端搬砖工程师。

文中如有错误,欢迎在评论区指正

0 人点赞