Object thường được tạo ra để đại diện cho các thực thể của thế giới thực, như người dùng, đơn hàng, v.v.

let user = {
  name: "John",
  age: 30
};

Và trong thế giới thực thì một user có thể hành động: chọn thứ gì đó từ giỏ hàng, login, logout,…

Các hành động thì được đại diện bằng Javascript function, cụ thể hơn là các function trong các thuộc tính.

Ví dụ Method (phương thức)

Để bắt đầu, chúng ta cùng dạy cho user nói hello:

let user = {
  name: "John",
  age: 30
};

user.sayHi = function() {
  alert("Hello!");
};

user.sayHi(); // Hello!

Ở đây chúng ta sử dụng một Function Expression để tạo một function và gán nó vào thuộc tính user.sayHi của object.

Tham khảo: Phần 2: Toán tử, câu lệnh điều kiện, vòng lặp, function, HOF, arrow function, call(), apply(), bind() trong Javascript

Sau đó chúng ta có thể gọi nó bằng cách user.sayHi(). Bây giờ user có thể nói!

Một function là thuộc tính của object thì được gọi là method (phương thức)

Vì thế, chúng ta được method sayHi của object user

Tất nhiên là chúng ta cũng có thể sử dụng một function đã được khai báo từ trước như thế này:

let user = {
  // ...
};

// first, declare
function sayHi() {
  alert("Hello!");
};

// Sau đó thêm một method
user.sayHi = sayHi;

user.sayHi(); // Hello!

Object-oriented programming – Lập trình hướng đối tượng

Khi chúng ta viết code sử dụng object (đối tượng) để đại diện cho các thực thể, chúng được gọi là lập trình hướng đối tượng, gắn gọn là “OOP”.

OOP là một điều to lớn, và nó có sự thú vị của riêng nó. Cách đẻ đúng thực thể? Cách để tổ chức và tương tác giữa các object? Những thứ đó được gọi là kiến trúc, và có một số quyển sách về chủ đề này mà mình recommend mọi người đọc như Design Patterns: Elements of Reusable Object-Oriented Software” của E. Gamma, R. Helm, R. Johnson, J. Vissides hoặc “Object-Oriented Analysis and Design with Applications” của G. Booch,…

Method shorthand

Có một cách viết ngắn gọn cho method trong object literal:

user = {
  sayHi: function() {
    alert("Hello");
  }
};

// method bây giờ nhìn xịn hơn phải không nào?
user = {
  sayHi() { // tương tự "sayHi: function()"
    alert("Hello");
  }
};

Như đã viết bên trên thì chúng ta có thể bỏ qua "function" và chỉ cần viết sayHi().

Thành thật mà nói thì 2 cách trên không hoàn toàn là giống nhau đâu. Có những khác biệt nhỏ liên quan đến kế thừa đối tượng, nhưng hiện tại chúng không quan trọng lắm. Trong hầu hết các trường hợp thì cú pháp ngắn gọn được ưu tiên hơn.

“this” trong method

Có một điều phổ biến là object method cần truy cập thông tin được lưu trong object để thực hiện một công việc gì đó.

Ví dụ, code bên trong user.sayHi() cần name của user.

Để truy cập object, một method có thể sử dụng từ khóa this.

Giá trị của this là object “trước dấu chấm”, chính là object mà dùng để gọi method.

Ví dụ:

let user = {
  name: "John",
  age: 30,

  sayHi() {
    // "this" là object hiện tại
    alert(this.name);
  }

};

user.sayHi(); // John

Ở đây trong suốt quá trình thực thi user.sayHi(), giá trị của this sẽ là user.

Về mặt kỹ thuật, có thể truy cập đến object mà không cần this, bằng cách tham chiếu trực tiếp đến biến.

let user = {
  name: "John",
  age: 30,

  sayHi() {
    alert(user.name); // "user" thay vì "this"
  }

}

Nhưng code như vậy thì không đáng tin cậy cho lắm. Nếu chúng ta quyết định copy user sang một biến khác, ví dụ admin = user và sau đó ghi đè user, như vậy thì nó sẽ truy cập đến một object sai.

Điều này được minh họa bên dưới:

let user = {
  name: "John",
  age: 30,

  sayHi() {
    alert( user.name ); // Điều này sẽ dẫn đến 1 lỗi
  }

};


let admin = user;
user = null; // ghi đè để làm thứ gì đó

admin.sayHi(); // TypeError: Cannot read property 'name' of null

Nếu chúng ta tạo this.name thay vì user.name bên trong alert thì code sẽ hoạt động đúng.

“this” không bị ràng buộc

Trong javascript, từ khóa this không hoạt động giống như hầu hết các ngôn ngữ lập trình khác. Nó có thể được sử dụng bên trong bất kỳ function nào, ngay cả khi nó không phải là một method của một object

Không có lỗi nào xảy ra trong đoạn code dưới đây:

function sayHi() {
  alert( this.name );
}
sayHi()

Lưu ý ở đây chúng ta chạy trong non-strict mode thì this lúc này là global object (như Window), nếu bạn ở strict mode thì sẽ gặp lỗi vì thisundefined.

'use strict'
function sayHi() {
  alert(this.name) // Cannot read property 'name' of undefined
}
sayHi()

Giá trị của this được tính toán suốt quá trình chạy code, dựa vào ngữ cảnh của nó.

Ví dụ, đây là cùng một function được gán cho 2 object khác nhau và nó this khác nhau khi gọi:

let user = { name: "John" };
let admin = { name: "Admin" };

function sayHi() {
  alert( this.name );
}

// sử dụng cùng 1 function trong 2 object
user.f = sayHi;
admin.f = sayHi;

// Chúng có this khác nhau
// "this" bên trong function là object trước dấu chấm
user.f(); // John  (this == user)
admin.f(); // Admin  (this == admin)

admin['f'](); // Admin (dùng dấu chấm hay dấu ngoặc vuông truy cập đến method - không thành vấn đề)

Quy luật đơn giản thôi: nếu obj.f() được gọi, thì thisobj trong suốt quá trình gọi f. Vì thế nó có thể là user hoặc admin ở ví dụ trên.

Hậu quả của việc không ràng buộc this

Nếu bạn đến từ một ngôn ngữ lập trình khác, thì bạn có thể sẽ quen thuộc ý tưởng “this ràng buộc”, nơi mà các method được định nghĩa bên trong object luôn luôn có this tham chiếu đến object đó.

Trong Javascript this thì “tự do”, giá trị của nó được tính toàn ngay tại lúc gọi và không dựa vào nơi method được khai báo, nhưng đúng hơn là đối tượng “trước dấu chấm”.

Concept của việc run-time tính toán this có cả ưu điểm và nhược điểm. Một mặt, một chức năng có thể được sử dụng cho các object khác nhau. Mặc khác, tính linh hoạt cao hơn tạo ra nhiều khả năng mắc sai lầm hơn.

Ở đây, quan điểm của chúng ta không phải phán xét ngôn ngữ này tốt hay xấu. Cách chúng ta nên tiếp cận là hiểu cách hoạt động của nó, cách để nhận được các lợi ích từ việc this không bị ràng buộc và tránh các vấn đề.

Arrow function không có “this”

Arrow function thì đặc biệt: chúng không có this của nó.

Nếu chúng ta tham chiếu đến this trong một arrow function, nó sẽ lấy this bên ngoài nó.

Ví dụ:

'use strict'
function handle1() {
  console.log(this) 
}

const handle2 = () => {
  console.log(this)
}

handle1() // undefined
handle2() // Window object

Một ví dụ khác, ở đây arrow() sẽ sử dụng this bên ngoài arrow(), tức lúc này là object user

'use strict'
let user = {
  firstName: 'Ilya',
  sayHi() {
    let arrow = () => alert(this.firstName)
    arrow()
  }
}

user.sayHi() // Ilya

Đó là tính năng đặc biệt của arrow function, nó hữu ích khi chúng ta tạo thực sự không muốn có một this riêng biệt.

Nếu không dùng arrow function thì lúc gọi this sẽ là this của function arrow() , mà this trong function arrow()undefined

'use strict'
let user = {
  firstName: 'Ilya',
  sayHi() {
    function arrow() {
      alert(this.firstName) // Cannot read property 'firstName' of undefined
    }
    arrow()
  }
}

user.sayHi()

Tóm lại

  • Function được lưu trữ bên trong thuộc tính object được gọi là “method” (phương thức).
  • Method cho phép object “hành động” như là object.doSomething().
  • Method có thể tham chiếu đến object bằng cách dùng this.

Giá trị của this được xác định lúc run-time.

  • Khi một function được khai báo, nó có thể sử dụng this, nhưng this không có giá trị cho đến khi function được gọi.
  • Một function có thể được copy giữa các object
  • Khi một function được gọi theo cú pháp “method”: object.method(), giá trị của this ở method trong suốt quá trình chạy là object.

Hãy lưu ý rằng arrow function không có this. Khi this được truy cập bên trong arrow function, nó sẽ được lấy từ bên ngoài.

Bài tập thực hành

1. Sử dụng “this” trong object literal

Ở đây function makeUser return một object.

Kết quả của việc truy cập đến ref của nó là gì? Tại sao?

Lưu ý là chạy trong strict mode

function makeUser() {
  return {
    name: "John",
    ref: this
  };
}

let user = makeUser();

alert( user.ref.name ); // What's the result?

Giải pháp

Câu trả lời: một lỗi

Thử lại:

function makeUser() {
  return {
    name: "John",
    ref: this
  };
}

let user = makeUser();

alert( user.ref.name ); // Error: Cannot read property 'name' of undefined

Đó là bởi vì quy tắc thiết lập this không phụ thuộc vào object khai báo. Chỉ thời điểm gọi mới quan trọng.

Ở đây giá trị của this bên trong makeUser()undefined, bởi bì nó được gọi như một function, nó không như một method với cú pháp “dấu chấm”.

Giá trị của this đại diện cho this của cả function, code block và object literal không ảnh hưởng đến nó.

Vậy nên ref: this nhận lấy this của function.

Chúng ta có thể viết lại function trên và return cùng this với giá trị undefined:

function makeUser(){
  return this; // Lúc này không có object literal
}

alert( makeUser().name ); // Error: Cannot read property 'name' of undefined

Như bạn có thể thấy kêt quả của alert( makeUser().name ) thì giống như kết quả của alert( user.ref.name ) từ ví dụ trên.

Đây là trường hợp ngược lại:

function makeUser() {
  return {
    name: "John",
    ref() {
      return this;
    }
  };
}

let user = makeUser();

alert( user.ref().name ); // John

Bây giờ nó hoạt động, bởi vì user.ref() là một method. Và giá trị của this được set là object trước dấu chấm ..

2. Tạo một máy tính

Tạo một object calculator với 3 method:

  • read() prompt 2 giá trị và lưu chúng như một thuộc tính object.
  • sum() return tổng của các giá trị được lưu.
  • mul() nhân các giá trị được lưu và return về kết quả.
let calculator = {
  // ... your code ...
};

calculator.read();
alert( calculator.sum() );
alert( calculator.mul() );

Giải pháp

let calculator = {
  sum() {
    return this.a + this.b;
  },

  mul() {
    return this.a * this.b;
  },

  read() {
    this.a = +prompt('a?', 0);
    this.b = +prompt('b?', 0);
  }
};

calculator.read();
alert( calculator.sum() );
alert( calculator.mul() );

3. Chaining – nối chuỗi

Có một ladder (cái thang) object cho phép đi lên và đi xuống:

let ladder = {
  step: 0,
  up() {
    this.step++;
  },
  down() {
    this.step--;
  },
  showStep: function() { // shows the current step
    alert( this.step );
  }
};

Bây giờ nếu chúng ta cần thực hiện một số lần gọi theo thứ tự dưới đây:

ladder.up();
ladder.up();
ladder.down();
ladder.showStep(); // 1

Sửa đổi code trong up, downshowStep sao cho có thể gọi thành một chuỗi như thế này:

ladder.up().up().down().showStep(); // 1

Cách viết như trên được sử dụng rộng rãi trong các thư viện Javascript

Giải pháp

Giải pháp là return chính object đó mỗi lần gọi

let ladder = {
  step: 0,
  up() {
    this.step++;
    return this;
  },
  down() {
    this.step--;
    return this;
  },
  showStep() {
    alert( this.step );
    return this;
  }
};

ladder.up().up().down().up().down().showStep(); // 1

Chúng ta cũng có thể viết từng dòng như thế này cho dễ đọc nếu chuỗi dài.

ladder
  .up()
  .up()
  .down()
  .up()
  .down()
  .showStep(); // 1

Tham khảo

Bài viết được dịch từ https://javascript.info/object-methods