Sử dụng magic method trong Javascript như PHP

159

Chào các bạn, không biết các bạn có biết không chứ kỹ năng chính của mình là PHP thôi. Nhưng dạo gần đây mình bắt đầu tìm hiểu sâu về Javascript để chuẩn bị cho dự án sắp tới của công ty mình đang làm việc. Cách tìm hiểu của mình là ánh xạ những gì mình học được với PHP sang Javascript.

Trong PHP có một khái niệm là magic methods, mình thử tìm hiểu xem trong Javascript có khái niệm nào tương tự như thế này không, nhưng rất tiếc là không. Trong khi đó magic methods là một trong những tính năng mình rất thích ở PHP, sang Javascript không có mình sợ mình không chịu nổi. Thế là mình mới mày mò tìm hiểu cách cài đặt magic methods ở Javascript. Rất may là sau ít giờ tìm hiểu, mình đã tìm ra, các bạn cùng tham khảo nhé.

Hiểu nhanh PHP magic methods

PHP magic methods là những hàm đặc biệt sử dụng trong lập trình hướng đối tượng PHP. Mỗi một magic method sẽ tự động được gọi khi đối tượng của nó xảy ra một sự kiện tương ứng.

methodĐược gọi khi
__construct()Khi khởi tạo đối tượng
__destruct()Khi hủy đối tượng
__get()Khi lấy giá trị một thuộc tính
__set()Khi gán giá trị cho một thuộc tính
__call()Khi gọi một method không tồn tại
__callStatic()Khi gọi một method static không tồn tại
__isset()Khi gọi hàm isset() hoặc empty() trên một thuộc tính không được phép truy cập.

Bảng trên mình chỉ liệt kê ra một số magic methods hay sử dụng, còn biết chi tiết hơn thì bạn tham khảo link sau nhé

>> Đọc thêm: PHP magic methods

Javascript không có magic methods nhưng có Proxy

Javascript tuy không hỗ trợ trực tiếp các magic methods trong class như PHP, nhưng lại hỗ trợ gián tiếp thông qua Proxy.

Proxy là một khái niệm trong Javascript được giới thiệu trong phiên bản ES6, cho phép chúng ta có thể can thiệp và làm thay đổi hành vi của một đối tượng như: truy xuất, thiết lập giá trị, thay đổi prototype,… Hiểu rõ hơn các bạn hãy xem qua ví dụ sau

Trong ví dụ này, mình tạo một class là Car, có một thuộc tính là name. Sau đó mình khởi tạo một đối tượng myCar và truy xuất đến thuộc tính name để log ra console, nhưng mình muốn thuộc tính name này phải viết in hoa khi log.

class Car {
  constructor (name) {
    // Gán giá trị cho thuộc tính name
    this.name = name
    
    // Điều kỳ diệu nằm ở đoạn này
    return new Proxy(this, {
      get (target, property) {
        // Nếu thuộc tính là `name` thì trả về name được viết in hoa
        // Một ví dụ cho việc làm thay đổi hành vi truy xuất dữ liệu của Proxy
        if (property == 'name') {
          return target[property].toUpperCase()
        }
        return target[property]
      }
    })
  }
}

let myCar = new Car('Honda civic 2019')
console.log(myCar.name) // HONDA CIVIC 2019

Trong ví dụ trên, mình gán giá trị cho thuộc tính name là Honda civic 2019, tuy nhiên khi truy xuất để in ra console thì lại có giá trị là HONDA CIVIC 2019 (đã được viết in hoa). Tại sao lại kỳ diệu đến như vậy, là do Proxy trong Javascript làm đó.

Hiểu nhanh về Proxy trong Javascript

Trước tiên là một số thuật ngữ:

  • target: Đối tượng bạn muốn làm thay đổi hành vi
  • traps: Những phương thức để làm thay đổi hành vi của đối tượng
  • handler: Một object chứa các traps.

Một số traps

Tên trapsĐược gọi khi
get()Khi truy xuất một thuộc tính
set()Khi gán giá trị cho thuộc tính
has()Khi sử dụng toán tử in với object

Mình chỉ gợi ý một số traps, muốn xem hết thì các bạn xem ở đây nhé

Để khởi tạo một Proxy chúng ta sử dụng cú pháp như sau

const variable = new Proxy(target, handler)

Sử dụng traps handler.get(target, property, receiver)

Traps get() nhận 3 tham số, tuy nhiên bạn chỉ cần quan tâm tới 2 tham số đầu tiên là được.

  • target: Là đối tượng cần thay đổi hành vi
  • property: thêm của thuộc tính sẽ được thay đổi hành vi
  • receiver: đối tượng sau khi đã được gán proxy

Xét thêm một ví dụ nữa, mình có một class User với hai thuộc tính là usernamepassword. Sau đó mình sẽ in ra màn hình hai thông tin này, tuy nhiên thông tin về password thì mình chỉ hiển thị 3 ký tự đầu tiên, còn các ký tự tiếp theo sẽ bị ẩn thành ký tự *.

let person = {
  username: 'admin',
  password: 'anhyeuem',
}

var proxyPerson = new Proxy(person, {
  get (target, property) {
    if (property == 'password') {
      let hiddenPassword = ''
      for (let i = 0; i < target.password.length; i++) {
        if (i < 3) {
          hiddenPassword += target.password[i]
        } else {
          hiddenPassword += '*'
        }
      }
      return hiddenPassword
    }
    return target[property]
  }
})

console.log(proxyPerson.username) // admin
console.log(proxyPerson.password) // anh*****

Mình lấy ví dụ cho cách sử dụng traps get thôi nhé, còn các traps khác thì cũng tương tự thôi.

Cài đặt magic methods như PHP với Js sử dụng Proxy

Mình có một class User, với 3 thuộc tính là firstname, lastname, email. Mình khởi tạo một đối tượng admin là một instance của class User.

Yêu cầu:

  1. Khi mình viết admin.fullname thì sẽ hiển thị ra Fullname của ông admin bằng cách nối lastname với firstname.
  2. Khi mình gán giá trị cho email thì phải kiểm tra xem email có hợp định dạng không, nếu không hợp định dạng thì throw Exception.
  3. Mô phỏng giống PHP magic methods nhất có thể

Phân tích:

Bài toán trên mình sẽ dùng đến Proxy. Khi thực hiện yêu cầu 1, mình sẽ tạo một traps get, kiểm tra xem nếu property là fullname thì return một string nối lastnamefirstname. Khi thực hiện yêu cầu 2, mình sẽ tạo một traps set, kiểm tra nếu property là email, thì giá trị của nó phải xuất hiện ít nhất một ký tự @. Để mô phỏng cho giống PHP nhất, thì mình sẽ tách traps get thành method __get(), và traps set thành method __set().

Code full

class User {
  constructor (lastname, firstname, email) {
    let proxyUser = new Proxy(this, this.magicMethods())
    proxyUser.lastname = lastname
    proxyUser.firstname = firstname
    proxyUser.email = email
    return proxyUser
  }
  
  __get(property) {
   if (property == 'fullname') {
     return `${this.lastname} ${this.firstname}`
   }
   return this[property]
  }
  
  __set(property, value) {
    if (property == 'email') {
      if (value.includes('@') == false) {
        throw 'email không hợp lệ'
      }
    }
    this[property] = value
  }
  
  magicMethods () {
    return {
      get (target, property) {
         return target.__get(property)
      },
      set (target, property, value) {
        try {
          target.__set(property, value)
          return true
        } catch (error) {
          throw error
        }
      }
    }
  }
}

let admin = new User('Phạm', 'Quang Bình', '[email protected]')
console.log(admin.fullname) // Phạm Quang Bình
console.log(admin.email) // [email protected]

let employer = new User('Phạm', 'Quang Minh', 'minhgmail.com') // Throw lỗi email không hợp lệ
console.log(employer.fullname) // Phạm Quang Minh
console.log(employer.email) // undefined

Kết luận

Bài viết trên tả lại thành quả nghiên cứu của mình sau nửa ngày nghiên cứu, nếu có gì thiếu sót rất mong nhận được gạch đá từ các bạn.