MST

星途 面试题库

面试题:JavaScript类与原型的性能优化

考虑一个场景,在JavaScript应用中有大量基于某个类创建的实例对象,这个类有一些方法和属性。从性能角度出发,说明使用类语法和原型语法在内存占用和方法调用效率上的差异,并且给出在这种场景下优化代码的策略。
44.8万 热度难度
编程语言JavaScript

知识考点

AI 面试

面试题答案

一键面试

类语法与原型语法在内存占用和方法调用效率上的差异

  1. 内存占用
    • 类语法:在ES6类语法中,每个实例对象都有自己的属性副本。对于方法,虽然类语法在幕后依然基于原型,但从开发者角度看,代码结构上像是每个实例都有自己的方法副本(实际上不是)。当有大量实例时,如果属性值是引用类型(如对象、数组),且每个实例的这些引用类型属性值不同,会占用较多内存。例如:
    class MyClass {
        constructor() {
            this.data = { value: 0 };
        }
    }
    const instances = Array.from({ length: 1000 }, () => new MyClass());
    
    这里1000个实例每个都有自己的data对象副本,占用内存较多。
    • 原型语法:使用原型语法时,属性可以直接定义在实例上(如果每个实例属性值不同),而方法定义在原型对象上。所有实例共享原型对象上的方法,这在内存占用上有优势。例如:
    function MyFunction() {
        this.data = { value: 0 };
    }
    MyFunction.prototype.myMethod = function() {
        // some code
    };
    const instances = Array.from({ length: 1000 }, () => new MyFunction());
    
    1000个实例共享myMethod,相比类语法如果每个实例有自己的方法副本(从代码结构角度),内存占用会少。
  2. 方法调用效率
    • 类语法:类语法在方法调用时,由于语法糖的存在,引擎需要进行一些额外的解析和转换工作,虽然现代JavaScript引擎对此做了很多优化,但理论上相比直接基于原型的调用会稍微慢一点。
    • 原型语法:直接基于原型的方法调用,在查找方法时,引擎直接在原型链上查找,相对更直接,方法调用效率略高(在大量方法调用场景下有体现)。

优化代码的策略

  1. 属性优化
    • 如果属性值每个实例不同,且是基本类型(如数字、字符串、布尔),在类语法或原型语法中直接定义在实例构造函数内。如:
    class MyClass {
        constructor(value) {
            this.basicValue = value;
        }
    }
    
    • 如果属性值是引用类型且每个实例相同,可将其定义为静态属性(类语法)或直接定义在构造函数的原型对象上(原型语法)。例如:
    class MyClass {
        static sharedData = { commonValue: 0 };
        constructor() {
            // use MyClass.sharedData
        }
    }
    
    使用原型语法:
    function MyFunction() {
        // use MyFunction.sharedData
    }
    MyFunction.sharedData = { commonValue: 0 };
    
  2. 方法优化
    • 无论使用类语法还是原型语法,尽量将方法定义在原型上以减少内存占用。在类语法中,这是默认行为(虽然语法糖隐藏了这一点)。例如:
    class MyClass {
        myMethod() {
            // code
        }
    }
    
    在原型语法中:
    function MyFunction() {}
    MyFunction.prototype.myMethod = function() {
        // code
    };
    
  3. 对象池模式:对于创建大量实例对象的场景,可以考虑对象池模式。提前创建一定数量的实例对象并放入对象池中,需要使用时从对象池中获取,使用完毕后再放回对象池,避免频繁创建和销毁实例对象带来的性能开销。例如:
    class ObjectPool {
        constructor(ClassType, poolSize) {
            this.ClassType = ClassType;
            this.pool = Array.from({ length: poolSize }, () => new ClassType());
        }
        acquire() {
            return this.pool.pop() || new this.ClassType();
        }
        release(instance) {
            this.pool.push(instance);
        }
    }
    
    使用时:
    class MyClass {}
    const pool = new ObjectPool(MyClass, 10);
    const instance1 = pool.acquire();
    // use instance1
    pool.release(instance1);