Prototypes trong JavaScript: Kế Thừa và OOP

Prototype là gì?

Prototype là cơ chế cốt lõi của JavaScript để thực hiện kế thừa (inheritance). Mọi object trong JavaScript đều có một prototype, và có thể kế thừa properties/methods từ prototype đó.

JavaScript không có Class truyền thống

JavaScript là prototype-based, không phải class-based như Java hay C++. Classes trong ES6+ chỉ là syntactic sugar trên prototype.

// "Class" trong JS thực chất là function
class Person {
    constructor(name) {
        this.name = name;
    }
}

// Tương đương với:
function Person(name) {
    this.name = name;
}

Prototype Chain

Mỗi object có một internal property [[Prototype]] (truy cập qua __proto__):

const obj = {};

console.log(obj.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null (end of chain)

Tìm kiếm property

JavaScript tìm property theo chuỗi prototype:

const person = {
    name: 'Alice'
};

console.log(person.name); // 'Alice' (tìm thấy trên chính object)
console.log(person.toString()); // '[object Object]' (tìm thấy trên Object.prototype)
console.log(person.notExist); // undefined (không tìm thấy trên chain)

Constructor Functions

Cách cũ (ES5)

function Person(name, age) {
    this.name = name;
    this.age = age;
}

// Thêm methods vào prototype
Person.prototype.sayHi = function() {
    console.log(`Hi, I'm ${this.name}`);
};

Person.prototype.getAge = function() {
    return this.age;
};

// Tạo instance
const alice = new Person('Alice', 25);
alice.sayHi(); // "Hi, I'm Alice"

// Kiểm tra
console.log(alice.__proto__ === Person.prototype); // true
console.log(Person.prototype.constructor === Person); // true

Lợi ích: Methods chỉ tạo 1 lần trên prototype, không duplicate cho mỗi instance.

// ❌ BAD: Mỗi instance có 1 copy của method
function Person(name) {
    this.name = name;
    this.sayHi = function() {
        console.log(`Hi, I'm ${this.name}`);
    };
}

// ✅ GOOD: Tất cả instances share 1 method
function Person(name) {
    this.name = name;
}
Person.prototype.sayHi = function() {
    console.log(`Hi, I'm ${this.name}`);
};

ES6 Classes

Cú pháp đẹp hơn nhưng vẫn dùng prototype:

class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    
    sayHi() {
        console.log(`Hi, I'm ${this.name}`);
    }
    
    getAge() {
        return this.age;
    }
}

const bob = new Person('Bob', 30);
bob.sayHi();

// Vẫn dùng prototype
console.log(bob.__proto__ === Person.prototype); // true
console.log(typeof Person); // "function"

Kế thừa (Inheritance)

Cách ES5

function Animal(name) {
    this.name = name;
}

Animal.prototype.eat = function() {
    console.log(`${this.name} is eating`);
};

function Dog(name, breed) {
    Animal.call(this, name); // Gọi parent constructor
    this.breed = breed;
}

// Kế thừa prototype
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

// Thêm method riêng
Dog.prototype.bark = function() {
    console.log('Woof!');
};

const dog = new Dog('Buddy', 'Golden');
dog.eat(); // "Buddy is eating" (từ Animal)
dog.bark(); // "Woof!" (từ Dog)

Cách ES6 (extends)

class Animal {
    constructor(name) {
        this.name = name;
    }
    
    eat() {
        console.log(`${this.name} is eating`);
    }
}

class Dog extends Animal {
    constructor(name, breed) {
        super(name); // Gọi parent constructor
        this.breed = breed;
    }
    
    bark() {
        console.log('Woof!');
    }
    
    // Override method
    eat() {
        super.eat(); // Gọi parent method
        console.log('Dog food yummy!');
    }
}

const dog = new Dog('Buddy', 'Golden');
dog.eat();
// Output:
// "Buddy is eating"
// "Dog food yummy!"
dog.bark(); // "Woof!"

Prototype Methods

Object.create()

Tạo object với prototype cụ thể:

const personPrototype = {
    sayHi() {
        console.log(`Hi, I'm ${this.name}`);
    }
};

const alice = Object.create(personPrototype);
alice.name = 'Alice';
alice.sayHi(); // "Hi, I'm Alice"

console.log(alice.__proto__ === personPrototype); // true

Object.getPrototypeOf()

Lấy prototype của object:

const proto = Object.getPrototypeOf(alice);
console.log(proto === personPrototype); // true

Object.setPrototypeOf()

Thay đổi prototype (⚠️ không khuyến khích - performance):

const newProto = {
    sayBye() {
        console.log('Bye!');
    }
};

Object.setPrototypeOf(alice, newProto);
alice.sayBye(); // "Bye!"

hasOwnProperty()

Kiểm tra property có phải của chính object không (không phải từ prototype):

class Person {
    constructor(name) {
        this.name = name;
    }
    
    sayHi() {
        console.log('Hi!');
    }
}

const person = new Person('Alice');

console.log(person.hasOwnProperty('name')); // true
console.log(person.hasOwnProperty('sayHi')); // false (trên prototype)

instanceof

Kiểm tra object có phải instance của constructor không:

console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true
console.log(dog instanceof Object); // true

Shadowing

Property trên object “che” property trên prototype:

const parent = {
    value: 10
};

const child = Object.create(parent);
console.log(child.value); // 10 (từ prototype)

child.value = 20; // Tạo property mới trên child
console.log(child.value); // 20 (từ chính object)
console.log(parent.value); // 10 (không thay đổi)

delete child.value;
console.log(child.value); // 10 (lại lấy từ prototype)

Built-in Prototypes

Array.prototype

const arr = [1, 2, 3];

// Methods từ Array.prototype
arr.push(4);
arr.map(x => x * 2);

// Thêm method cho tất cả arrays
Array.prototype.first = function() {
    return this[0];
};

console.log([1, 2, 3].first()); // 1

⚠️ Cảnh báo: Không nên modify built-in prototypes trong production!

String.prototype

// Polyfill cho includes() (ES6)
if (!String.prototype.includes) {
    String.prototype.includes = function(search) {
        return this.indexOf(search) !== -1;
    };
}

Ví dụ thực tế

1. Plugin System

class Editor {
    constructor() {
        this.content = '';
    }
    
    setText(text) {
        this.content = text;
    }
}

// Plugin: Spell checker
Editor.prototype.checkSpelling = function() {
    console.log('Checking spelling...');
    // Logic here
};

// Plugin: Auto save
Editor.prototype.autoSave = function() {
    console.log('Auto saving...');
    // Logic here
};

const editor = new Editor();
editor.setText('Hello world');
editor.checkSpelling();
editor.autoSave();

2. Mixin Pattern

// Mixin để thêm functionality
const canFly = {
    fly() {
        console.log(`${this.name} is flying`);
    }
};

const canSwim = {
    swim() {
        console.log(`${this.name} is swimming`);
    }
};

class Bird {
    constructor(name) {
        this.name = name;
    }
}

// Thêm mixins
Object.assign(Bird.prototype, canFly);

class Duck extends Bird {
    constructor(name) {
        super(name);
    }
}

// Duck vừa bay vừa bơi
Object.assign(Duck.prototype, canSwim);

const duck = new Duck('Donald');
duck.fly(); // "Donald is flying"
duck.swim(); // "Donald is swimming"

3. Factory Pattern với Prototype

const carPrototype = {
    start() {
        console.log(`${this.brand} ${this.model} started`);
    },
    stop() {
        console.log('Car stopped');
    }
};

function createCar(brand, model) {
    const car = Object.create(carPrototype);
    car.brand = brand;
    car.model = model;
    return car;
}

const car1 = createCar('Toyota', 'Camry');
const car2 = createCar('Honda', 'Civic');

car1.start(); // "Toyota Camry started"
car2.start(); // "Honda Civic started"

4. Memoization với Prototype

class Calculator {
    constructor() {
        this.cache = {};
    }
    
    fibonacci(n) {
        if (n in this.cache) {
            return this.cache[n];
        }
        
        if (n <= 1) return n;
        
        const result = this.fibonacci(n - 1) + this.fibonacci(n - 2);
        this.cache[n] = result;
        return result;
    }
}

// Thêm shared method
Calculator.prototype.clear = function() {
    this.cache = {};
};

const calc = new Calculator();
console.log(calc.fibonacci(10)); // 55

Class vs Prototype: Khi nào dùng gì?

Dùng Class khi:

  • Code dễ đọc, gần với OOP truyền thống
  • Cần extends và super
  • Team quen với class-based languages
class User {
    constructor(name) {
        this.name = name;
    }
    
    greet() {
        return `Hello, ${this.name}`;
    }
}

Dùng Prototype trực tiếp khi:

  • Cần performance tối ưu
  • Dynamic thêm methods
  • Tạo objects không cần constructor
const userMethods = {
    greet() {
        return `Hello, ${this.name}`;
    }
};

function createUser(name) {
    const user = Object.create(userMethods);
    user.name = name;
    return user;
}

Best Practices

  1. Ưu tiên Class syntax (ES6+)
// ✅ Dễ đọc
class Animal {
    constructor(name) {
        this.name = name;
    }
}
  1. Không modify built-in prototypes
// ❌ Nguy hiểm
Array.prototype.myMethod = function() {};

// ✅ Tạo utility function
function myArrayMethod(arr) {}
  1. Sử dụng Object.create() thay vì proto
// ❌ Deprecated
obj.__proto__ = prototype;

// ✅ Standard
const obj = Object.create(prototype);
  1. Luôn set constructor khi kế thừa (ES5)
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // ✅ Đừng quên

Kết luận

Prototype là trái tim của JavaScript OOP:

  • Hiểu prototype chain giúp debug tốt hơn
  • Classes ES6 là syntax sugar trên prototype
  • Prototype cho phép kế thừa linh hoạt
  • Dùng đúng pattern cho từng use case

Nắm vững prototype sẽ giúp bạn hiểu sâu JavaScript và viết code hiệu quả hơn!