大厂面试题:两种面向对象编程方式,详解手写new过程,使用单例模式减少内存开销

198 阅读5分钟

构造函数和原型方式创建对象

为什么要在JavaScript中创建对象?

减少性能消耗: 有一些对象,有着共同的行为,如果使每个对象都有自己的函数,那么就会影响性能

在JavaScript可以用方式创建对象?

1. 对象字面量

对象字面量是一种简单的对象创建方式,适用于简单的对象或单例对象。这种方式通过直接定义对象的属性和方法,比较直观。

//这是一个简单的可以嘎嘎叫的Duck
const duck = {
  nickname: '小黄',
  singsing: function () {
    console.log('嘎嘎');
  }
};
duck.singsing();

2. 构造函数和原型

构造函数和原型链是JavaScript中实现面向对象编程的主要方式,适用于需要创建多个相似对象的场景。通过构造函数,可以为每个实例设置初始属性,而通过原型链,可以为所有实例共享方法,节省内存。

// 构造函数
function Duck(nickname) {
  this.nickname = nickname; 
}

// 在Duck的原型上定义方法singsing
Duck.prototype.singsing = function () {
  console.log('嘎嘎嘎');
};

// 实例化对象
const duck = new Duck('鸭王');

// 调用实例方法
duck.singsing();

手写new操作符过程

new 操作符的过程

  1. 创建一个新的空对象
  2. 将这个空对象的原型指向构造函数的原型
  3. 将构造函数中的 this 绑定到新创建的对象
  4. 执行构造函数的代码,并返回对象

1、创建一个新的空对象

这一步创建了一个新的空对象 obj,将来会作为新实例返回

const obj = {};

2、将这个空对象的原型指向构造函数的原型

  • 将空对象原型指向构造函数原型的方法有两种

    将新对象 obj 的原型指向构造函数的原型,这一步确保了通过 new 创建的对象可以继承构造函数原型上的方法和属性。

  1. 使用 Object.setPrototypeOf 方法

    Object.setPrototypeOf()  静态方法可以将一个指定对象的原型(即内部的 [[Prototype]] 属性)设置为另一个对象或者 null

    //创建两个类
    const obj = {};
    const parent = { foo: 'bar' };
    
    //指定obj原型为parent
    Object.setPrototypeOf(obj, parent);
    
    console.log(obj.foo);
    
  2. 使用 对象的__proto__属性和prototype属性

    .prototype 是 JavaScript 中每个函数对象默认拥有的一个属性。这个属性指向一个对象,所有由该函数创建的实例都可以共享这个对象的属性和方法。在面向对象编程中,prototype 是实现继承和共享方法的重要机制

    .prototype 的工作原理
    构造函数:当定义一个构造函数(如 Duck)时,JavaScript 引擎会自动为它创建一个 prototype 属性,指向一个初始对象(称为原型对象)
    实例的原型链:当使用 new 关键字创建一个实例时,实例会有一个隐式属性 __proto__,它指向构造函数的 prototype 对象
    方法查找机制:当访问实例的一个属性或方法时,JavaScript 会先查找实例自身的属性或方法。如果没有找到,它会继续查找实例的 __proto__(即构造函数的 prototype 对象)上的属性或方法。

    const obj = {};
    const parent = { foo: 'bar' };
    
    obj.__proto__ = parent.prototype
    
    console.log(obj.foo);
    

3、将构造函数中的 this 绑定到新创建的对象

apply 方法调用一个具有给定 this 值的函数,并以一个数组(或类数组对象)作为参数。

 func.apply(thisArg, [argsArray]
//func:要调用的函数。
 //thisArg:调用函数时使用的 this 值。
//argsArray:数组或类数组对象,表示函数调用时的参数列表。
function greet(greeting, punctuation) {
   console.log("嘎嘎")
}

const context = { user: '小黄' }

// 使用 apply 调用 greet,this 指向 context,参数为 ['Hello', '!']
greet.apply(context, ['Hello', '!'])

4、执行构造函数的代码,并返回对象

apply第二个参数是数组,在将this绑定到obj对象时,同时也将数组内容作为参数传给构造函数。

return obj

简单单例模式

为什么要使用单例模式

有的类只需要实例化一次,可以减少性能消耗,更方便管理

单例模式的流程

  1. 创建构造函数
  2. 创建原型方法
  3. 挂载静态方法
  4. 创建实例
// 单例模式,有的类只实例化一次,性能更好,更方便管理
var Singleton = function (name) {
  this.name = name
}


Singleton.prototype.getName = function () {
  console.log(this.name)
}

// getInstance直接挂载在Singleton上,相当Java中的静态方法
Singleton.getInstance = function (name) {
  if (!this.instance) {
    // 静态的属性,静态的属性,所有实例共享
    this.instance = new Singleton(name)
  }
  return this.instance
}

let obj1 = Singleton.getInstance('铠甲勇士')
let obj2 = Singleton.getInstance('帝皇铠甲')
console.log(obj1 == obj2, obj1.name, obj2.name)

1. 创建构造函数

该函数接收一个 name 参数并将其赋值给实例的 name 属性。

var Singleton = function (name) {
  this.name = name
  // this.instance = null 用判断实例有无被创建,且作保存内存地址的作用
}

2. 创建原型方法

getName 方法被添加到 Singleton 的原型中,所有 Singleton 的实例都可以访问这个方法。

Singleton.prototype.getName = function () {
  console.log(this.name)
}

3. 挂载静态方法

getInstance 方法被直接挂载到 Singleton 函数上。它在第一次调用时创建一个 Singleton 实例,并将该实例存储在 Singleton 的静态属性 instance 中。之后的调用都返回同一个实例。

// getInstance直接挂载在Singleton上,相当Java中的静态方法
Singleton.getInstance = function (name) {
  if (!this.instance) {
    // 静态的属性,静态的属性,所有实例共享
    this.instance = new Singleton(name)
  }
  return this.instance
}

4. 创建实例

我们通过Singleton.getInstance 方法创建实例,并验证结果

let obj1 = Singleton.getInstance('铠甲勇士')
let obj2 = Singleton.getInstance('帝皇铠甲')
console.log(obj1 == obj2, obj1.name, obj2.name)

优化单例模式

关键点

  • Singleton.getInstance 是一个静态方法,它确保 Singleton 只被实例化一次。
  • Singleton.instance 是一个静态属性,存储唯一的 Singleton 实例。
  • 每次调用 Singleton.getInstance 方法时,如果实例不存在,则创建一个新的 Singleton 实例;如果实例已存在,则返回现有实例。

修改与优化

  1. 私有化构造函数:通过抛出错误阻止直接调用构造函数,强制使用 getInstance 方法创建实例。
  2. 重置方法:添加一个方法来重置单例实例(例如在测试环境中)。
  3. 更灵活的参数处理:允许传递更多参数给单例实例。

优化后代码