MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JavaScript类和原型的代码优化思路

2021-03-244.1k 阅读

JavaScript类和原型的基础知识回顾

在深入探讨代码优化思路之前,我们先来回顾一下JavaScript中类和原型的基本概念。

原型(Prototype)

JavaScript是基于原型的编程语言。每个对象都有一个原型对象,对象可以从其原型对象继承属性和方法。可以通过__proto__属性访问对象的原型。例如:

const obj = {name: 'John'};
console.log(obj.__proto__); 

原型链是这样工作的:当访问对象的某个属性时,如果对象本身没有该属性,JavaScript会沿着原型链向上查找,直到找到该属性或者到达原型链的顶端(null)。

函数对象有一个特殊的属性prototype,当使用构造函数创建新对象时,新对象的__proto__会指向构造函数的prototype。例如:

function Person(name) {
    this.name = name;
}
Person.prototype.sayHello = function() {
    console.log(`Hello, I'm ${this.name}`);
};
const john = new Person('John');
john.sayHello(); 

类(Class)

ES6引入了类的语法糖,它基于原型系统构建。类本质上是函数的一种特殊形式。例如:

class Person {
    constructor(name) {
        this.name = name;
    }
    sayHello() {
        console.log(`Hello, I'm ${this.name}`);
    }
}
const mary = new Person('Mary');
mary.sayHello(); 

这里的class只是语法糖,在底层依然是使用原型系统。constructor方法对应构造函数,而定义在类中的方法会被添加到类的prototype上。

代码优化思路之一:合理使用原型方法

减少重复代码

在传统的基于原型的编程中,将方法定义在构造函数内部会导致每个实例都有一份方法的副本,这会浪费内存。例如:

function Animal(name) {
    this.name = name;
    this.speak = function() {
        console.log(`${this.name} makes a sound.`);
    };
}
const dog = new Animal('Buddy');
const cat = new Animal('Whiskers');

在上述代码中,dogcat都有自己独立的speak方法副本。如果有大量的Animal实例,这会占用大量内存。

优化方法是将方法定义在原型上:

function Animal(name) {
    this.name = name;
}
Animal.prototype.speak = function() {
    console.log(`${this.name} makes a sound.`);
};
const dog = new Animal('Buddy');
const cat = new Animal('Whiskers');

这样,所有Animal实例共享speak方法,节省了内存。

在ES6类中,同样的原则适用。方法应该定义在类体中,而不是在constructor中:

class Animal {
    constructor(name) {
        this.name = name;
    }
    speak() {
        console.log(`${this.name} makes a sound.`);
    }
}
const lion = new Animal('Leo');
const tiger = new Animal('Rajah');

利用原型链继承

合理利用原型链继承可以减少代码冗余,提高代码的可维护性。例如,假设有一个Vehicle基类和Car子类:

function Vehicle(type) {
    this.type = type;
}
Vehicle.prototype.move = function() {
    console.log(`${this.type} is moving.`);
};
function Car(model) {
    Vehicle.call(this, 'car');
    this.model = model;
}
Car.prototype = Object.create(Vehicle.prototype);
Car.prototype.constructor = Car;
Car.prototype.drive = function() {
    console.log(`Driving a ${this.model}`);
};
const myCar = new Car('Toyota Corolla');
myCar.move(); 
myCar.drive(); 

在ES6类中,继承变得更加简洁:

class Vehicle {
    constructor(type) {
        this.type = type;
    }
    move() {
        console.log(`${this.type} is moving.`);
    }
}
class Car extends Vehicle {
    constructor(model) {
        super('car');
        this.model = model;
    }
    drive() {
        console.log(`Driving a ${this.model}`);
    }
}
const myNewCar = new Car('Honda Civic');
myNewCar.move(); 
myNewCar.drive(); 

通过继承,Car类可以复用Vehicle类的move方法,减少了代码重复。

代码优化思路之二:理解类和原型的内存管理

避免不必要的引用

当对象之间存在复杂的引用关系时,可能会导致内存泄漏。例如,假设我们有一个Widget类,它包含一个对Document对象的引用:

class Widget {
    constructor() {
        this.documentRef = document;
    }
}
const widget = new Widget();
// 即使widget不再使用,由于documentRef的引用,widget及其相关内存不会被垃圾回收

为了避免这种情况,当Widget不再需要document引用时,应该手动将其设置为null

class Widget {
    constructor() {
        this.documentRef = document;
    }
    destroy() {
        this.documentRef = null;
    }
}
const myWidget = new Widget();
// 当不再需要myWidget时
myWidget.destroy();

原型对象的内存占用

原型对象本身也会占用一定的内存。如果原型对象上有大量的属性和方法,并且这些属性和方法很少被使用,那么可以考虑将不常用的部分延迟加载。例如:

function BigObject() {
    // 初始化必要的属性
}
Object.defineProperty(BigObject.prototype, 'heavyMethod', {
    value: function() {
        // 这里是复杂且耗时的操作
        console.log('Performing heavy operation');
    },
    enumerable: false,
    configurable: true
});
const bigObj = new BigObject();
// 只有当调用bigObj.heavyMethod()时,才会真正加载和执行该方法

在ES6类中,可以使用getter来实现类似的延迟加载效果:

class BigObject {
    constructor() {
        // 初始化必要的属性
    }
    get heavyMethod() {
        // 这里是复杂且耗时的操作
        console.log('Performing heavy operation');
    }
}
const myBigObj = new BigObject();
// 只有当访问myBigObj.heavyMethod时,才会执行相关操作

代码优化思路之三:提高原型链查找效率

减少原型链长度

原型链过长会导致属性查找变慢。例如,假设有一个多层继承的结构:

function Base() {}
Base.prototype.baseMethod = function() {
    console.log('Base method');
};
function Intermediate() {}
Intermediate.prototype = Object.create(Base.prototype);
Intermediate.prototype.intermediateMethod = function() {
    console.log('Intermediate method');
};
function Leaf() {}
Leaf.prototype = Object.create(Intermediate.prototype);
Leaf.prototype.leafMethod = function() {
    console.log('Leaf method');
};
const leafObj = new Leaf();
// 当调用leafObj.baseMethod()时,需要沿着原型链查找多层

为了提高查找效率,可以尽量扁平化原型链。如果Intermediate类没有独特的逻辑,可以直接让Leaf类继承Base类:

function Base() {}
Base.prototype.baseMethod = function() {
    console.log('Base method');
};
function Leaf() {}
Leaf.prototype = Object.create(Base.prototype);
Leaf.prototype.leafMethod = function() {
    console.log('Leaf method');
};
const newLeafObj = new Leaf();
// 现在调用newLeafObj.baseMethod()查找层级减少

缓存属性查找结果

如果在一个方法中多次访问同一个属性,并且该属性在原型链上,那么可以缓存该属性的查找结果。例如:

function Shape() {
    this.color = 'black';
}
Shape.prototype.draw = function() {
    const ctx = this.getContext(); 
    // 假设getContext是在原型链上查找的方法
    ctx.fillStyle = this.color;
    // 这里可能多次使用ctx
};

优化后:

function Shape() {
    this.color = 'black';
}
Shape.prototype.draw = function() {
    let ctx;
    if (!this._cachedCtx) {
        ctx = this.getContext();
        this._cachedCtx = ctx;
    } else {
        ctx = this._cachedCtx;
    }
    ctx.fillStyle = this.color;
    // 这里可能多次使用ctx
};

通过缓存getContext的结果,减少了在原型链上的查找次数,提高了性能。

代码优化思路之四:结合ES6类和传统原型的优势

在ES6类中使用传统原型技巧

虽然ES6类提供了简洁的语法,但有时传统原型的一些技巧依然有用。例如,在ES6类中,可以手动修改prototype来添加一些特殊的属性或方法:

class MyClass {
    constructor() {
        this.value = 0;
    }
    increment() {
        this.value++;
    }
}
Object.defineProperty(MyClass.prototype, 'doubleValue', {
    get: function() {
        return this.value * 2;
    },
    enumerable: true,
    configurable: true
});
const myObj = new MyClass();
myObj.increment();
console.log(myObj.doubleValue); 

这里通过Object.definePropertyMyClassprototype上添加了一个getter属性doubleValue,这在某些复杂场景下可以提供更灵活的功能。

利用类的语法糖简化原型操作

ES6类的语法糖使得原型相关的操作更加直观和易于理解。例如,在传统原型编程中,设置构造函数的prototype时需要小心处理constructor的指向:

function OldStyle() {}
OldStyle.prototype = {
    method: function() {
        console.log('Old style method');
    }
};
OldStyle.prototype.constructor = OldStyle; 

而在ES6类中,这一切都由语言自动处理:

class NewStyle {
    method() {
        console.log('New style method');
    }
}
const newObj = new NewStyle();

利用类的语法糖,可以减少因手动处理原型和constructor指向而导致的错误,提高代码的可读性和可维护性。

代码优化思路之五:处理类和原型中的动态行为

动态添加和删除原型方法

在某些情况下,需要在运行时动态添加或删除原型方法。例如,在一个游戏开发场景中,根据游戏的不同阶段,可能需要为角色添加不同的技能:

class Character {
    constructor(name) {
        this.name = name;
    }
    basicAttack() {
        console.log(`${this.name} performs a basic attack.`);
    }
}
// 在游戏的某个阶段添加新技能
function addSpecialSkill(characterClass) {
    characterClass.prototype.specialAttack = function() {
        console.log(`${this.name} performs a special attack.`);
    };
}
const player = new Character('Hero');
player.basicAttack(); 
addSpecialSkill(Character);
player.specialAttack(); 

同样,也可以动态删除原型方法:

function removeSpecialSkill(characterClass) {
    delete characterClass.prototype.specialAttack;
}
removeSpecialSkill(Character);
// player.specialAttack(); 这会导致错误,因为方法已被删除

动态修改原型链

虽然不常见,但有时可能需要动态修改原型链。例如,在一个插件系统中,根据加载的插件不同,对象的行为可能会发生变化:

function BaseObject() {}
BaseObject.prototype.baseFunction = function() {
    console.log('Base function');
};
function Plugin1() {}
Plugin1.prototype.plugin1Function = function() {
    console.log('Plugin 1 function');
};
function Plugin2() {}
Plugin2.prototype.plugin2Function = function() {
    console.log('Plugin 2 function');
};
function loadPlugin(baseObj, plugin) {
    if (plugin === 'plugin1') {
        baseObj.__proto__ = Object.create(Plugin1.prototype, {
            constructor: {
                value: baseObj.constructor,
                enumerable: false,
                writable: true,
                configurable: true
            }
        });
    } else if (plugin === 'plugin2') {
        baseObj.__proto__ = Object.create(Plugin2.prototype, {
            constructor: {
                value: baseObj.constructor,
                enumerable: false,
                writable: true,
                configurable: true
            }
        });
    }
}
const base = new BaseObject();
base.baseFunction(); 
loadPlugin(base, 'plugin1');
base.plugin1Function(); 

动态修改原型链需要非常小心,因为它可能会破坏代码的预期行为,并且会影响性能,所以只有在必要时才使用。

代码优化思路之六:性能测试与分析

使用性能测试工具

为了验证代码优化的效果,需要使用性能测试工具。在JavaScript中,可以使用console.time()console.timeEnd()来简单测量代码块的执行时间。例如,比较在构造函数内部定义方法和在原型上定义方法的性能:

// 在构造函数内部定义方法
console.time('constructorMethod');
function ConstructorMethod() {
    this.method = function() {
        // 一些简单操作
        return 1 + 1;
    };
}
for (let i = 0; i < 100000; i++) {
    const obj = new ConstructorMethod();
    obj.method();
}
console.timeEnd('constructorMethod');

// 在原型上定义方法
console.time('prototypeMethod');
function PrototypeMethod() {}
PrototypeMethod.prototype.method = function() {
    // 一些简单操作
    return 1 + 1;
};
for (let i = 0; i < 100000; i++) {
    const obj = new PrototypeMethod();
    obj.method();
}
console.timeEnd('prototypeMethod');

通过这种方式,可以直观地看到在原型上定义方法的性能优势。

更专业的性能测试可以使用工具如Benchmark.js。例如:

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>Performance Test</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/benchmark/2.1.4/benchmark.min.js"></script>
</head>

<body>
    <script>
        function ConstructorMethod() {
            this.method = function() {
                return 1 + 1;
            };
        }
        function PrototypeMethod() {}
        PrototypeMethod.prototype.method = function() {
            return 1 + 1;
        };
        const suite = new Benchmark.Suite;
        suite.add('Constructor method', function() {
            const obj = new ConstructorMethod();
            obj.method();
        })
          .add('Prototype method', function() {
                const obj = new PrototypeMethod();
                obj.method();
            })
          .on('cycle', function(event) {
                console.log(String(event.target));
            })
          .on('complete', function() {
                console.log('Fastest is'+ this.filter('fastest').map('name'));
            })
          .run({ 'async': true });
    </script>
</body>

</html>

Benchmark.js提供了更详细和准确的性能测试结果,帮助我们更好地优化代码。

分析性能瓶颈

通过性能测试,可能会发现一些性能瓶颈。例如,如果原型链过长导致属性查找变慢,那么可以通过前面提到的减少原型链长度的方法来优化。另外,如果在原型链查找过程中,有大量的对象属性访问,那么缓存属性查找结果可能会显著提高性能。

同时,要注意浏览器的优化策略。不同的浏览器对原型链查找、类的实现等有不同的优化方式。例如,V8引擎(Chrome浏览器使用)对热点函数(经常调用的函数)有特殊的优化,会将其编译为更高效的机器码。因此,在优化代码时,也要考虑目标浏览器的特性。

代码优化思路之七:错误处理与类和原型的健壮性

原型方法中的错误处理

在原型方法中,需要合理处理错误,以确保对象的健壮性。例如,假设我们有一个FileReader类,它的原型方法用于读取文件:

class FileReader {
    constructor(file) {
        this.file = file;
    }
    readFile() {
        try {
            // 模拟文件读取操作
            if (!this.file.exists) {
                throw new Error('File does not exist');
            }
            console.log('File read successfully');
        } catch (error) {
            console.error('Error reading file:', error.message);
        }
    }
}
const reader = new FileReader({ exists: false });
reader.readFile(); 

通过在原型方法readFile中使用try - catch块,当文件不存在时,能够捕获错误并进行适当的处理,而不是让程序崩溃。

防止原型污染

原型污染是一个严重的问题,它可能导致安全漏洞和意外的行为。例如,恶意代码可能会尝试修改原型对象:

// 恶意代码
Object.prototype.newMaliciousProperty = 'This is malicious';
function SomeClass() {}
const instance = new SomeClass();
console.log(instance.newMaliciousProperty); 

为了防止原型污染,可以使用Object.freeze()方法冻结原型对象。例如:

function SafeClass() {}
Object.freeze(SafeClass.prototype);
// 以下操作会被忽略,因为原型已被冻结
SafeClass.prototype.newProperty = 'This will not work';
const safeInstance = new SafeClass();
// safeInstance.newProperty不会存在

另外,在使用第三方库时,要注意库是否可能导致原型污染,并且尽量避免在全局对象的原型上添加属性和方法。

通过以上从基础知识回顾到各个优化思路的探讨,我们可以在JavaScript中更有效地使用类和原型,编写出高效、健壮且易于维护的代码。无论是在小型项目还是大型应用中,这些优化思路都能为我们的开发工作带来显著的提升。