Back
Featured image of post Javascript Prototype

Javascript Prototype

How Javascript implements OOP in details

Javascript & OOP

Trước khi trình bày cụ thể về cách mà Javascript triển khai OOP, các bạn có thể tham khảo bài viết trước của mình có trình bày sơ bộ về các cách triển khai OOP (gồm class-based & prototype-based)

Vốn dĩ từ ban đầu Javascript được tạo ra với mục đích chính là hướng mọi người tới với Functional Programming, nên trong những phiên bản đầu tiên của Javascript thì không có khái niệm class (vì Javascript đâu có phải Java), nhưng vật đổi sao dời thì những nhà phát triển đã nhận ra OOP là một phát minh quan trọng không thể thay thế. Nên các nhà phát triển Javascript đã dần dần cài đặt thêm các chức năng hỗ trợ hướng đối tượng vào Javascript. Để giữ tình mềm dẻo, linh hoạt của Javascript thì sự lựa chọn Prototype là an toàn nhất.

Everything is Object

Có một câu nói về Javascript thế này : “Everything is an object, or a primitive”. Về cơ bản thì mọi thứ ở trong Javascript đều là object (object trong object oriented programming), ngoại trừ một vài thứ khác như là: number, strings, null, undefined, boolean, symbols.

Để hiểu một cách đơn giản thì object trong Javascript giống như một hash map, một key-value pair. Chúng ta có thể truy cập đến value thông qua một key unique. Chúng ta có thể dễ dàng truy cập tới các phần tử mà object lưu trữ thông qua key và dot notation (toán tử .)

Encapsulation

Nói về hướng đối tượng, tính đóng gói được đặt lên hàng đầu đối với các ngôn ngữ định hướng đối tượng. Tuy nhiên Javascript làm điều này rất linh hoạt và loosely (lỏng lẻo). Với Java, tính đóng gói được thể hiển ở việc định nghĩa một class.

public class Human {
    private String firstName;
    private String lastName;
    private int age;
    constructor(String firstName,String lastName,int age){
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }

    public void speak(){
        System.out.println('I am Human');
    }
}

Một object được tạo ra từ class Human sẽ chỉ có quyền truy cập tới những attributes & method có access modifier là public mà thôi. Còn đối với Javascript thì khái niệm access modifier có lẽ là không tồn tại, bởi vì:

  • Everything is objet
  • Everykey in object is public

Đối với Javascript, ta có thể gọi class là một template thì chính xác hơn là một class, chúng ta có thể khai báo một template theo 2 cách.

function createHuman(firstName,lastName,age){
    let obj = {}
    obj.firstName = firstName
    obj.lastName = firstName
    obj.age = age
    obj.speak = function (){
        console.log(`I am Human, you can call me ${obj.firstName}`)
    }
    return obj
}

Tuy nhiên cách này lại có vẻ mất tự nhiên và hơi dài dòng, thay vào đó Javascript cũng hỗ trợ phiên bản xịn xò hơn để có thể ít viết code lại nhờ vào từ khoá this và toán tử new đi kèm với tên của class được viết hoa chữ cái đầu (mà không phải sử dụng tên hàm). Lúc này thì chúng ta có thể xem đây là một constructor (Javascript còn có cả từ khoá class)

function Human(firstName,lastName,age){
    this.firstName = firstName
    this.lastName = firstName
    this.age = age
    this.speak = function (){
        console.log(`I am Human, you can call me ${this.firstName}`)
    }
}
let human = new Human('Harry','Potter',18)
let human_ex = new Human('Hermione','Granger',18)
human.speak()
human_ex.speak()
// I am Human, you can call me Harry
// I am Human, you can call me Hermione

Cách thứ 2 là sử dụng thực thể tối thượng là Object và phương thức create (chúng ta sẽ gọi đây là một clone, gần gần giống như kế thừa)

let human_wife = Object.create(human)
human_wife.firstName = 'Ginny'
human_wife.lastName = 'Weasly'
human_wife.age = 18
console.log(human)
console.log(human_wife)

Thứ chúng ta nhận được ở console là

trông có vẻ là đúng nhưng mà speak của human_wife lại biến mất đâu rồi, còn [[Prototype]] là cái gì nữa vậy nhỉ?

Prototype

Prototype là có thể hiểu là một cơ chế phân cấp, để quản lý các method của object. Giả sử khi mình gọi một method của một object, method này không tồn tại trong object hiện tại, thì object này sẽ tìm cách đưa yêu cầu lên cấp cao hơn tìm kiếm method có tên đó. Giống như khi bạn bị đánh, bạn đánh không lại người ta, bạn sẽ gọi anh của bạn, anh của bạn vẫn đánh không lại, thì anh của bạn sẽ gọi một ông anh nào đó nữa để giành lại công bằng cho bạn, và câu chuyện gọi hội sẽ tiếp tục đến khi không còn ai để gọi (đã gọi đến cấp cao nhất mà vẫn không có method đó). Prototype là cơ chế phân cấp như vậy. Việc truy xuất các phần tử (có thể là attribute & method ) dựa theo một quy tắc là prototype chain cho phép truy xuất các phần tử dần dần từ cấp bậc thấp đến cấp bậc cao - cao nhất là Object.

Object có thể truy xuất tới prototype của cấp lớn hơn thông qua key __proto__. Tất cả các thuộc tính được kế thừa đều nằm ở trong __proto__ object. Lấy ví dụ cũ, chúng ta sẽ thêm một vài chi tiết và kết quả

console.log(human)
console.log(human_wife)
console.log(human_wife.__proto__)

Điều này đồng nghĩa với việc là clone object có khả năng truy cập và chỉnh sửa original object

console.log(human)
human_wife.__proto__.lastName = 'ginny\'s husband'
console.log(human)

Một điều thú vị là nếu bạn expand bằng việc bấm nút tam giác ngược ở dòng thứ 2 thì sẽ thấy data cũng bị thay đổi theo (vì lúc này trình duyệt sẽ lấy dữ liệu hiện tại của object - đã bị thay đổi)

Javascript cài đặt cơ chế kế thừa thông qua prototype nên cũng tồn đọng vấn đề tương tự. Nhưng trước khi đến với ví dụ về kế thừa thông qua từ khoá extends thì mình sẽ giới thiệu về từ khoá class trước. Thực ra sử dụng từ khoá class đi kèm với new thì bản chất ở bên dưới là sử dụng Object.create

class Human {
    constructor(firstName, lastName, age) {
        this.firstName = firstName
        this.lastName = firstName
        this.age = age
    }
    speak = function () {
        console.log(`I am Human, you can call me ${this.firstName}`)
    }
}

thì sẽ được thay thế bằng ở phần backgroud (bằng chứng là kết quả khi console 2 object được tạo ra 2 cách giống nhau Prototype cơ bản)

function Human(firstName,lastName,age){
    this.firstName = firstName
    this.lastName = firstName
    this.age = age
    this.speak = function (){
        console.log(`I am Human, you can call me ${this.firstName}`)
    }
}
// lưu ý nếu chúng ta sử dụng cách này chúng ta đang khai báo prototype của function thay vì khai báo như method - cách trên
// Human.prototype.speak = function (){
//     console.log(`I am Human, you can call me ${this.firstName}`)
// }

Có thể một vài điều khó hiểu là tại sao mình khi thì dùng __proto__ và khi thì dùng prototype. 2 cách này đều được sử dụng nhằm mục đích truy cập vào prototype của một object. Tuy nhiên __proto__ sử dụng với mục đích truy cập prototype của object gốc (object được clone sẽ sử dụng __proto__ để truy cập tới prototype của object có bậc cao hơn - gọi anh xã hội). Còn prototype thì sử dụng để định nghĩa các prototype của class (chính xác hơn là constructor function) hiện tại. Xét thử ví dụ sau

function Human(firstName,lastName,age){
    this.firstName = firstName
    this.lastName = firstName
    this.age = age
}
// works
Human.prototype.speak =function (){
    console.log(`I am Human, you can call me ${this.firstName}`)
}
let human_wife = Object.create(human)
human_wife.firstName = 'Ginny'
human_wife.lastName = 'Weasly'
human_wife.age = 18
// bug, err eh ?
human_wife.prototype.saylove = function (){
    console.log(`I am ${this.firstName}, in love with u!`)
}

Điều cần chú ý là prototype là một property của một function, còn __proto__property của object. Mọi thứ có trong prototype của một constructor function sẽ được nhân bản tự động và copy khi từ khoá new được gọi (function không chỉ đơn giản là function, mà là constructor function, nếu gọi function mà không có từ khoá new thì kết quả trả về là undefined, như ví dụ đã đề cập ở trước). Chúng có thể sử dụng thực thể tối thượng là Object để có quyền chỉnh sửa prototype.

Object.setPrototypeOf(human_wife, {
    saylove: function () {
        console.log(`I am ${this.firstName}, in love with u!`)
    }
})
human_wife.saylove()
// bug, err eh ?
human_wife.speak()

Và lúc này thì prototype được copy từ human đã bị replace hoàn toàn, chúng ta có thể giữ lại bằng viêc sử dụng rest operator (…)

Conclusion

Tóm lại, prototype khác với property, khi chúng ta sử dụng dot notation (.) thì javascript sẽ dựa vào cơ chế prototype chain để lookup giá trị. Về bản chất prototype là một object, chứa thông các property đã được xác định trước. Chúng ta có thể khai báo prototype thông qua func.prototype. Khi khởi tạo một object, object sẽ có quyền truy cập tới prototype của object gốc (thông thường là Object). Nếu sử dụng Objet.create, thì object truyền vào hàm object chính là prototype. Chúng ta có thể truy cập tới prototype của object gốc thông qua property__proto__. Javascript khiến cho việc sử dụng OOP rất linh hoạt, dễ dàng , không cứng nhắc như Java, nhưng đồng thời cũng có những bất lợi là quà tặng kèm. Từ phiên bản ECMA2015 thì Javascript đã hỗ trợ các từ khoá như class,super,constructor, extends ,… để giúp viết code hướng đối tượng dễ dàng hơn (thay vì sử dụng Object.create)


References & more resources

P/S:

Nếu có gì sai sót xin gửi email cho mình để cập nhật, xin cảm ơn!

Licensed under CC BY-NC-SA 4.0
Hello, I am Son
Built with Hugo
Theme Stack designed by Jimmy