JS创建对象 方法比较及利弊分析

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
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享