Details of the object model

You’re reading the English version of this content since no translation exists yet for this locale. Help us translate this article!

JavaScript là một ngôn ngữ dựa trên đối tượng và nguyên mẫu, hơn là dựa trên class. Bởi vì điều này, làm nó kém rõ ràng về cách mà JavaScript cho phép chúng ta tạo cấu trúc cây của đối tượng cũng như tạo sự kế thừa cho thuộc tính và giá trị của chúng. Chương này cố gắng làm rõ đặc điểm này.

Chương này giả định rằng bạn đã hiểu căn bản JavaScript và bạn đã sử dụng hàm của JavaScript để tạo đối tượng đơn giản.

Ngôn ngữ Class-based vs. Ngôn ngữ prototype-based

Ngôn ngữ dựa theo class, ví dụ Java và C++, tách biệt 2 thực thể chính: lớp (class) và thực thể (instance).

  • Một class định nghĩa tất cả các thuộc tính mà nó quy định tính chất của tập các object (xem phương thức và trường trong Java, hay thành viên trong C++, như là những thuộc tính). Một class là sự trù tượng hơn là một thành viên cụ thể của tập các object mà nó mô tả. Ví dụ, Employee class có thể biểu diễn tập hợp của tất cả employee.
  • Mặt khác một thực thể (instance) là một thực thể của một class, và là một trong những thành viên của nó. Ví dụ, Victoria có thể là một thực thể của Employee class, nó đại diện cho một cá nhân như là một employee. Một thực thể có chính xác cùng thuộc tính của class cha của nó (không hơn, không kém).

Ngôn ngữ dựa trên nguyên mẫu, như JavaScript, không tách biệt điều này, nó đơn giản chỉ là những object. Một ngôn ngữ dựa trên nguyên mẫu có ký hiệu của một prototypical object, một object được dùng làm mẫu mà từ mẫu đó nó tạo ra thuộc tính cho object mới. Hơn nữa, bất kỳ object nào cũng có thể được liên kết để dùng làm nguyên mẫu của một object khác, điều này cho phép object thứ hai chia sẽ thuộc tính với object thứ nhất.

Định nghĩa một class

Ngôn ngữ dựa trên class, bạn định nghĩa một class bằng từ khóa định nghĩa class riêng biệt rõ ràng. Trong đó bạn có thể chỉ định phương thức đặc biệt, gọi là hàm dựng, để khởi tạo thực thể của class. Một hàm dựng có thể khởi tạo giá trị cho thuộc tính của thực thể và thực hiện những xử lý phù hợp tại thời điểm đó. Bạn có thể dùng toán tử new cùng với tên class để tạo thực thể của class.

JavaScript cũng dùng mô hình tương tự, nhưng không có từ khóa riêng biệt để định nghĩa class (với phiên bản trước ES6). Thay vào đó, bạn có thể định nghĩa hàm dựng để tạo object với tập hợp các giá trị và thuộc tính ban đầu. Bất kỳ hàm JavaScript nào cũng có thể dùng như là constructor. Bạn có thể dùng từ toán tử new với hàm dựng để tạo object mới.

Class con và sự kế thừa

Trong ngôn ngữ dựa trên class, bạn tạo cây thứ tự của class thông qua những định nghĩa class. Khi định nghĩa một class, bạn có thể chỉ định class đang tạo là class con của một class khác đang tồn tại. Class con kế thừa tất cả các thuộc tính của class cha và có thể thêm vào những thuộc tính mới hoặc chỉnh sửa thuộc tính được kế thừa. Ví dụ, giả sử class Employee chỉ có thuộc tính namedept, và Manager là class con của Employee và định nghĩa thêm thuộc tính reports. Trong trường hợp này thực thể của class Manager sẽ có ba thuộc tính: name, dept, and reports

JavaScript cài đặt tính kế thừa bằng cách cho phép bạn liên kết một object mẫu với bất kỳ hàm dựng nào. Vì vậy, bạn có thể tạo chính xác như ví dụ EmployeeManager trên, nhưng thuật cú pháp sẽ nhưng hơi khác một tí. Trước tiên bạn định nghĩa hàm dựng Employee, và trong thân hàm khởi tạo thuộc tính namedept. Tiếp theo bạn định nghĩa hàm dựng Manager, mà nó gọi hàm dựng Employee và khởi tạo thuộc tính reports. Cuối cúng bạn gán object mới kế thừa từ Employee.prototype như prototype cho hàm được dựng Manager. Sau đó, khi tạo một thực thể Manager mới, nó sẽ kế thừa thuộc tính namedept của Employee.

Việc thêm và xóa những thuộc tính

Trong ngôn ngữ dựa trên class, bạn thông thường tạo một class lúc biên dịch và rồi bạn khởi tạo thực thể của class tại lúc biên dịch hoặc lúc thực thi. Bạn không thể thay đổi số lượng và kiểu của thuộc tính của class sau khi bạn định nghĩa class. Trong JavaScript, bạn có thể thêm hoặc xóa thuộc tính của bất kỳ object nào. Nếu bạn thêm một thuộc tính của một object mà được sử dụng như prototype của một tập các object, những object đó cũng sẽ có những thuộc tính mới.

Tóm tắt lại những khác biệt

Sau đây là bản tóm tắt ngắn những khác biệt. Phần còn lại của chương mô tả chi tiết việc sử dụng hàm dựng của JavaScript và prototype để tạo cấu trúc cây đối tượng và so sánh với phương pháp tương tự trong Java.

So sánh class-based (Java) và prototype-based (JavaScript)
Class-based (Java) Prototype-based (JavaScript)
Class và thực thể của nó là hai đối tượng riêng biệt. Tất cả đối tượng có thể kế thừa từ một đối tượng khác.
Dùng cú pháp riêng của class để định nghĩa class; khởi tạo thực thể của class dùng hàm dựng của class. Định nghĩa và tạo một tập các đối tượng chỉ với hàm dựng.
Tạo object đơn với toán tử  new. Cùng cách như class-based.
Xây dựng cấu trúc cây object bằng cách sử dụng định nghĩa class để tạo class con của class đang tồn tại. Xây dựng cấu trúc cây object bằng cách gán một object như prototype của hàm dựng.
Kế thừa thuộc tính theo cơ chế class nối tiếp. Kế thừa những thuộc tính theo cơ chế prototype nối tiếp.
Khi định nghĩa class, bạn chỉ định tất cả các thuộc tính của các thực thể của class. Và không thể thêm thuộc tính mới lúc thực thi. Hàm dựng và prototype chỉ định giá trị ban đầu của thuộc tính. Và có thể thêm hoặc xoá thuộc tính động trên từng đối tượng hoặc toàn bộ các object.

Ví dụ Employee

Phần còn lại của chương này sử dụng cấu trúc cây nhân viên được trình bày như hình bên dưới.

Cấu trúc cây object đơn giản:

  • Employee có những thuộc tính name (mà có giá trị mặc định là một chuỗi rỗng) và dept (mà giá trị mặc định là chuỗi "general").
  • Manager kế thừa từ Employee. Nó thêm thuộc tính reports (mà giá trị mặc định là một mảng rỗng sẽ dùng để lưu một mảng các Employee object)
  • WorkerBee cũng kế thừa từ Employee. Nó thêm thuộc tính projects (mà có giá trị mặc định là một mảng rỗng sẽ dùng để lưu trữ một mảng các giá trị kiểu chuỗi).
  • SalesPerson kế thừa từ WorkerBee. Nó thêm thuộc tính quota (mà có giá trị mặc định là 100). Nó cũng ghi đè giá trị của thuộc tính dept với giá trị "sales", để chỉ ra rằng tất cả salespersons thuộc cùng phòng ban.
  • Engineer kế thừa WorkerBee. Nó thêm vào thuộc tính machine (mà có giá trị mặc định là chuỗi rỗng) và cũng ghi đè thuộc tính dept với giá trị "engineering".

Tạo hệ thống cấp bậc

Có một vài cách để định nghĩa hàm dựng thích hợp để cài đặt hệ thống cấp bậc của nhân viên. Cách bạn muốn chọn để định nghĩa phụ thuộc phần lớn vào những gì bạn muốn làm trong ứng dụng của bạn.

Phần này sẽ trình bày cách định nghĩa rất đơn giản để minh họa cách thực hiện sự kế thừa. Trong định nghĩa này, bạn không thể chỉ định giá trị nào cho thuộc tính khi bạn tạo một object. Một cách đơn giản một object mới được ra và nhận những thuộc tính với giá trị mặc định, sau đó bạn có thể thay đổi giá trị của những thuộc tính đó.

Trong ứng dụng thực tế, bạn có thể định nghĩa hàm dựng mà cho phép bạn cung cấp những giá trị của thuộc tính tại thời điểm tạo object (xem thêm More flexible constructors). Bây giờ, ta chỉ dùng cách đơn giản để minh họa sự kế thừa.

Việc định nghĩa Employee trong Java và JavaScript thì khá tương tự. Sự khác biệt chỉ là bạn cần chỉ định kiểu của mỗi thuộc tính trong Java nhưng không cần trong JavaScript (bởi vì Java là ngôn ngữ kiểu ràng buộc mạnh trong khi JavaScript là ngôn ngữ kiểu rành buộc yếu.

JavaScript

function Employee() {
  this.name = '';
  this.dept = 'general';
}


Java

public class Employee {
   public String name = "";
   public String dept = "general";
}

Việc định nghĩa ManagerWorkerBee chỉ ra sự khác nhau trong cách chỉ định đối tượng cấp cao hơn trong chuỗi kế thừa nối tiếp. Trong JavaScript, bạn có thể thêm nguyên mẫu (trở thành class cha) như là thuộc tính prototype của hàm dựng, rồi ghi đè prototype.constructor lên hàm dựng. Bạn có thể làm điều này bất kỳ thời điểm nào sau khi đã định nghĩa hàm dựng. Trong Java, bạn chỉ định class cha trong khi định nghĩa class. Bạn không thể thay đổi class cha sau khi định nghĩa class.

JavaScript

function Manager() {
  Employee.call(this);
  this.reports = [];
}
Manager.prototype = Object.create(Employee.prototype);
Manager.prototype.constructor = Manager;

function WorkerBee() {
  Employee.call(this);
  this.projects = [];
}
WorkerBee.prototype = Object.create(Employee.prototype);
WorkerBee.prototype.constructor = WorkerBee;


Java

public class Manager extends Employee {
   public Employee[] reports = 
       new Employee[0];
}



public class WorkerBee extends Employee {
   public String[] projects = new String[0];
}


 

Việc định nghĩa EngineerSalesPerson giúp tạo objects theo thứ tự giảm dần từ Employee rồi xuống WorkerBee. Một object được tạo ra từ lớp con sẽ có tất cả thuộc tính của lớp cha ở trên trong chuỗi nối tiếp đó. Hơn nữa những định nghĩa ở lớp con có thể ghi đè những giá trị kế thừa từ lớp cha.

JavaScript

function SalesPerson() {
   WorkerBee.call(this);
   this.dept = 'sales';
   this.quota = 100;
}
SalesPerson.prototype = Object.create(WorkerBee.prototype);
SalesPerson.prototype.constructor = SalesPerson;

function Engineer() {
   WorkerBee.call(this);
   this.dept = 'engineering';
   this.machine = '';
}
Engineer.prototype = Object.create(WorkerBee.prototype)
Engineer.prototype.constructor = Engineer;


Java

public class SalesPerson extends WorkerBee {
   public String dept = "sales";
   public double quota = 100.0;
}


public class Engineer extends WorkerBee {
   public String dept = "engineering";
   public String machine = "";
}

Với việc sử dụng cách định nghĩa này, bạn có thể tạp hóa việc khởi tạo giá trị cho thuộc tính của thực thể với những giá trị mặc định cho các thuộc tính. Hình tiếp theo sẽ minh họa việc sử dụng cách định nghĩa trong JavaScript để tạo những object mới và hiển thị giá trị của thuộc tính của những object đó.

Lưu Ý: Thuật ngữ thực thể (instance) có ý nghĩa kỹ thuật cụ thể trong ngôn ngữ class-based. Trong ngôn ngữ này, một thực thể (instance) là một thực thể riêng biệt của class và là tách biệt với class. Trong JavaScript, "thực thể" không có khái niệm kỹ thuật riêng bởi vì JavaScript không tách biệt sự khác nhau giữa class và instance. Tuy nhiên, khi nói về JavaScript, "thực thể" có thể được sử dụng để ám chỉ những object được tạo ra bằng hàm dựng. Vì vậy, trong ví dụ này, bạn có thể nói jane là một thực thể của Engineer. Tương tự, mặc dù những thuật ngữ parent, child, ancestor, và descendant không có ý nghĩa chính thống trong JavaScript, bạn có thể sử dụng chúng để ám chỉ những đối tượng cấp cao hơn hoặc thấp hơn trong cây thừa kế.

Việc tạo những đối tượng bằng cách đơn giản

Cây object

Cây sau được tạo ra bằng những câu lệnh ở bên phải.

 

Individual objects = Jim, Sally, Mark, Fred, Jane, etc.
"Instances" created from constructor

var jim = new Employee; 
// Parentheses can be omitted if the
// constructor takes no arguments.
// jim.name is ''
// jim.dept is 'general'

var sally = new Manager;
// sally.name is ''
// sally.dept is 'general'
// sally.reports is []

var mark = new WorkerBee;
// mark.name is ''
// mark.dept is 'general'
// mark.projects is []

var fred = new SalesPerson;
// fred.name is ''
// fred.dept is 'sales'
// fred.projects is []
// fred.quota is 100

var jane = new Engineer;
// jane.name is ''
// jane.dept is 'engineering'
// jane.projects is []
// jane.machine is ''

Thuộc tính của Object

Phần này mô tả cách mà những thuộc tính của những object có thể được kế thừa từ object khác trong chuỗi prototype và điều gì xãy ra khi bạn thêm một thuộc tính lúc thực thi.

Việc kế thừa thuộc tính

Giả định bạn tạo mark object như là một WorkerBee với câu lệnh sau:

var mark = new WorkerBee;

khi JavaScript thấy toán tử new, nó sẽ tạo một object mới và âm thầm đặt những giá trị của thuộc tính bên trong [[Prototype]] cho WorkerBee.prototype và truyền đối tượng mới tạo như là giá trị của từ khóa this cho hàm dựng WorkerBee. Thuộc tính [[Prototype]] xác định chuỗi prototype được dùng để trả về cho những giá trị của thuộc tính. Một khi những thuộc tính được thiết lập, JavaScript trả về đối tượng mới và những câu lệnh gán thiết lập giá trị thuộc tính của đối tượng mark.

Quy trình này không đặt giá trị cho những thuộc tính kế thừa từ chuỗi prototype trực tiếp vào trong đối tượng mark. Khi bạn lấy giá trị của một thuộc tính, JavaScript đầu tiên sẽ kiểm tra xem nếu giá trị tồn tại trực tiếp trên chính đối tượng đó. Nếu có, giá trị sẽ được trả về. Nếu không tồn tại trên chính đối tượng đó, JavaScript kiểm tra chuỗi prototype (sử dụng thuộc tính [[Prototype]]). Nếu một object trong chuỗi prototype có giá trị cho thuộc tính, giá trị sẽ được trả về. Nếu không tồn tại thuộc tính, JavaScript sẽ trả lời là không có thuộc tính. Bằng cách này, đối tượng mark có những thuộc tính và những giá trị sau:

mark.name = '';
mark.dept = 'general';
mark.projects = [];

Đối tượng mark được gán những giá trị cục bộ cho thuộc tính name và  dept bằng hàm dựng Employee. Nó được gán giá trị cục bộ cho thuộc tính projects bằng hàm dựng WorkerBee. Điều này giúp tạo ra sự kế thừa của các thuộc tính trong JavaScript. Một vài điểm tinh tế trong qui trình này được mô tả trong Property inheritance revisited.

Bởi vì hàm dựng không cho phép bạn chỉ định những giá trị riêng của thực thể, những thông tin này là chung. Những giá trị của thuộc tính là mặc định được chia sẻ cho tất cả các đối tượng được tạo ra từ WorkerBee. Tất nhiên bạn có thể thay đổi giá trị của bất kỳ thuộc tính nào. Bạn có thể làm điều đó trên mark object bằng những câu lệnh sau:

mark.name = 'Doe, Mark';
mark.dept = 'admin';
mark.projects = ['navigator'];

Thêm thuộc tính

Trong JavaScript, bạn có thể thêm thuộc tính cho object lúc thực thi. Bạn không bị rành buộc phải sử dụng chỉ những thuộc tính được cung cấp bởi hàm dựng. Để thêm một thuộc tính cụ thể cho một object riêng lẻ, bạn có thể gán giá trị cho thuộc tính của object đó như sau:

mark.bonus = 3000;

Bây giờ object mark có thuộc tính bonus, những thực thể của WorkerBee không có thuộc tính này.

Nếu bạn thêm thuộc tính mới vào một object mà đang được sử dụng làm prototype cho một hàm dựng, bạn đang thêm thuộc tính đó cho tất cả các object mà kế thừa từ prototype. Ví dụ, bạn có thể thêm thuộc tính specialty cho tất cả employees bằng câu lệnh sau:

Employee.prototype.specialty = 'none';

Ngay sau khi thực thi câu lệnh này, đối tượng mark cũng có thuộc tính specialty với giá trị "none". Hình sau minh họa việc thêm thuộc tính vào prototype của Employee và sau đó ghi đè nó lên prototype của Engineer.


Adding properties

Những hàm dựng linh hoạt hơn

Những hàm dựng đã được trình bày ở trên không cho phép chúng ta chỉ định giá trị cho thuộc tính khi chúng ta tạo thực thể. Với Java, bạn có thể cung cấp đối số cho hàm dựng để khởi tạo giá trị của thuộc tính cho những thực thể. Hình sau minh họa cách để thực hiện điều này.
 


Việc chỉ định thuộc tính trong hàm dựng, cách 1

Bảng minh họa sau trình bày cách định nghĩa những đối tượng này trong Java và JavaScript.

JavaScript

Java

function Employee(name, dept) {
  this.name = name || '';
  this.dept = dept || 'general';
}

 

 

 

 

 

public class Employee {
   public String name;
   public String dept;
   public Employee () {
      this("", "general");
   }
   public Employee (String name) {
      this(name, "general");
   }
   public Employee (String name, String dept) {
      this.name = name;
      this.dept = dept;
   }
}
function WorkerBee(projs) {
 
 this.projects = projs || [];
}
WorkerBee.prototype = new Employee;

 

 

 

public class WorkerBee extends Employee {
   public String[] projects;
   public WorkerBee () {
      this(new String[0]);
   }
   public WorkerBee (String[] projs) {
      projects = projs;
   }
}
 
function Engineer(mach) {
   this.dept = 'engineering';
   this.machine = mach || '';
}
Engineer.prototype = new WorkerBee;

 

 

 

public class Engineer extends WorkerBee {
   public String machine;
   public Engineer () {
      dept = "engineering";
      machine = "";
   }
   public Engineer (String mach) {
      dept = "engineering";
      machine = mach;
   }
}

Những định nghĩa này trong JavaScript sử dụng cú pháp đặc biệt để thiết lập giá trị mặc định:

this.name = name || '';

Trong JavaScript toán tử luận lý OR (||) ước lượng giá trị đầu tiên của đối số. Nếu đối số được chuyển đổi thành true, toán tử trả về giá trị của đối số đó. Ngược lại toán tử trả về đối số thứ hai. Vì vậy, đoạn mã trên kiểm tra xem nếu name có giá trị cho thuộc tính name. Nếu có, nó thiết lập giá trị đó cho this.name. Ngược lại, nó đặt giá trị chuỗi rỗng vào this.name. Chương này sử dụng cú pháp vắn tắt, tuy nhiên nó có thể làm chúng ta khó hiểu lúc ban đầu.

Lưu Ý: Cách này có thể không hoạt động như mong đợi nếu hàm dựng được gọi với đối số mà được chuyển đổi thành false (ví dụ như 0 (zero) và chuỗi rỗng (""). trong trường hợp này giá trị mặc định sẽ được chọn)

Với những định nghĩa này, khi bạn tạo một thực thể của một object, bạn có thể chỉ định giá trị cho những thuộc tính được định nghĩa cục bộ. Bạn có thể sử dụng những câu lệnh sau để tạo một Engineer:

var jane = new Engineer('belau');

Thuộc tính của Jane bây giờ là:

jane.name == '';
jane.dept == 'engineering';
jane.projects == [];
jane.machine == 'belau';

Lưu ý là với những định nghĩa này, bạn không thể chỉ định giá trị khởi tạo cho một thuộc tính được kế thừa như thuộc tính name. Nếu bạn muốn chỉ định một giá trị khởi tạo cho thuộc tính được kế thừa trong JavaScript, bạn cần để thêm câu lệnh cho hàm khởi tạo.

Hàm tạo đã tạo một đối tượng chung và rồi sau đó những giá trị được khởi tạo cho những thuộc tính cục bộ trực tiếp của đối tượng mới. Bạn có thể khởi tạo thêm những thuộc tính bằng cách trực tiếp gọi những hàm dựng khác của chuỗi prototype. Hình sau sẽ minh họa điều đó.


Những giá trị đang được chỉ định trong hàm dựng, hình 2

Hãy xem xét một trong những định nghĩa một cách chi tiết. Đây là định nghĩa mới cho hàm dựng Engineer:

function Engineer(name, projs, mach) {
  this.base = WorkerBee;
  this.base(name, 'engineering', projs);
  this.machine = mach || '';
}

Giả định rằng bạn tạo một đối tượng Engineer mới như sau:

var jane = new Engineer('Doe, Jane', ['navigator', 'javascript'], 'belau');

JavaScript thực thi theo những bước sau:

  1. Toán tử new tạo một object và thiết lập giá trị __proto__ của object mới tạo với giá trị là Engineer.prototype.
  2. Toán tử mới sẽ truyền object mới cho hàm dựng Engineer như là giá trị của từ khóa this.
  3. Hàm dựng tạo một thuộc tính mới được gọi là base cho đối tượng đó và gán giá trị của hàm dựng WorkerBee cho thuộc tính base. Điều này làm hàm dựng trở thành một phương thức của đối tượng Engineer. Tên của thuộc tính base thì không có gì đặc biệt. Bạn có thể dùng bất kỳ tên hợp lệ nào như là classBase, _base,... base đơn giản chỉ là một con trỏ đến hàm.
  4. Hàm dựng gọi phương thức base, truyền vào hàm dựng như những đối số ("Doe, Jane" and ["navigator", "javascript"]"engineering"). Việc sử dụng tường minh "engineering" trong hàm dựng mà tất cả đối tượng Engineer có cùng giá trị cho thuộc tính được kế thừa dept, và giá trị ghi đè giá trị mà được kế thừa từ Employee.
  5. Bởi vì base là phương thức của Engineer, JavaScript gắn từ khóa this với object được tạo ra ở bước 1. Vì thế hàm WorkerBee truyền các giá trị "Doe, Jane""engineering" cho hàm dựng Employee. Dựa trên giá trị trả về của hàm dựng Employee, hàm WorkerBee sử dụng những đối số còn lại để thiết lập giá trị cho thuộc tính projects.
  6. Dựa trên giá trị trả về của phương thức base, hàm dựng Engineer khởi tạo thuộc tính machine của đối tượng với giá trị "belau".
  7. Dựa trên giá trị trả về của hàm dựng, JavaScript gán đối tượng cho biến jane.

Bạn có thể nghĩ rằng, việc phải gọi hàm dựng WorkerBee bên trong hàm dựng Engineer, để đảm bảo việc cài đặt sự kế thừa cho tất cả đối tượng Engineer. Điều này không quá quan trọng. Việc gọi hàm dựng WorkerBee để đảm bảo tất cả đối tượng Engineer được tạo ra với các thuộc tính được chỉ định trong tất cả các hàm dựng được gọi. Tuy nhiên nếu bạn thêm thuộc tính vào prototype của Employee hoặc WorkerBee, những thuộc tính đó không được kế thừa bởi đối tượng Engineer. Ví dụ, giả sử bạn có đoạn mã sau:

function Engineer(name, projs, mach) {
  this.base = WorkerBee;
  this.base(name, 'engineering', projs);
  this.machine = mach || '';
}
var jane = new Engineer('Doe, Jane', ['navigator', 'javascript'], 'belau');
Employee.prototype.specialty = 'none';

Đối tượng jane không kế thừa thuộc tính specialty. Bạn vẫn cần cài đặt tường minh prototype để đảm bảo sự kế thừa động. Giả sử bạn có đoạn mã sau:

function Engineer(name, projs, mach) {
  this.base = WorkerBee;
  this.base(name, 'engineering', projs);
  this.machine = mach || '';
}
Engineer.prototype = new WorkerBee;
var jane = new Engineer('Doe, Jane', ['navigator', 'javascript'], 'belau');
Employee.prototype.specialty = 'none';

Bây giờ thuộc tính specialty của đối tượng jane là "none".

Một cách khác để kế thừa là dùng hàm phương thức call() / apply(). Hai đoạn mã sau là tương đương:

function Engineer(name, projs, mach) {
  this.base = WorkerBee;
  this.base(name, 'engineering', projs);
  this.machine = mach || '';
}
function Engineer(name, projs, mach) {
  WorkerBee.call(this, name, 'engineering', projs);
  this.machine = mach || '';
}

Việc sử dụng phương thức call() làm cho code rõ ràng hơn và base không còn cần thiết nữa.

Xem lại sự kế thừa thuộc tính

Đoạn trên đã mô tả cách hàm dựng và prototype trong JavaScript cung cấp cơ chế kế thừa và cây kế thừa. Trong phần này, chúng ta sẽ nói về điểm tinh tế mà thật sự chưa phù hợp để mô tả ở những phần trước.

Dùng Giá Trị Cục Bộ hay là Giá Trị Được Kế  Thừa

Khi bạn truy xuất thuộc tính của object, JavaScript thực hiện những bước sau:

  1. Kiểm tra nếu tồn tại thuộc tính trực tiếp, nó trả về giá trị của thuộc tính đó.
  2. Nếu không có thuộc tính trực tiếp, kiểm tra trong chuỗi prototype (sử dụng thuộc tính __proto__).
  3. Nếu một object trong chuỗi prototype chứa giá trị cho một thuộc tính đang tìm thì trả về giá trị của thuộc tính đó.
  4. Nếu không tồn tại thuộc tính như vậy, thì object đó không có thuộc tính đang truy xuất.

Kết quả nhận được của những bước trên phụ thuộc vào cách bạn định nghĩa đối tượng. Ví dụ sau minh họa những cách định nghĩa đối tượng:

function Employee() {
  this.name = '';
  this.dept = 'general';
}

function WorkerBee() {
  this.projects = [];
}
WorkerBee.prototype = new Employee;

Với cách định nghĩa này, giả định rằng bạn tạo đối tượng amy là thực thể của WorkerBee với đoạn lệnh sau:

var amy = new WorkerBee;

Đối tượng amy có một biến cục bộ (riêng) là projects. Các thuộc tính namedept thì không là thuộc tính riêng của amy mà được lấy ra từ thuộc tính __proto__. Vì vậy, đối tượng amy có những thuộc tính với những giá trị sau:

amy.name == '';
amy.dept == 'general';
amy.projects == [];

Giả định rằng bạn thay đổi giá trị của thuộc tính name trong prototype của Employee:

Employee.prototype.name = 'Unknown';

Bạn mong muốn rằng giá trị mới sẽ được thiết lập cho tất cả các thực thể của Employee. Nhưng điều nay không xãy ra.

Khi bạn tạo bất kỳ thực thể nào của đối tượng từ Employee, thì thực thể đó sẽ tạo thuộc tính name riêng trực tiếp trên chính thực thể đó (cụ thể đó là giá trị rỗng). Điều này có nghĩa là khi bạn thiết lập giá trị cho prototype của  WorkerBee bằng một đối tượng kiểu Employee, thì WorkerBee.prototype có lưu giá trị riêng cho thuộc tính name. Vì vậy, khi JavaScript tìm kiếm thuộc tính name của đối tượng amy (tạo ra từ WorkerBee), JavaScript tìm kiếm giá riêng cho thuộc tính đó trong WorkerBee.prototype. Do đó nó không tìm kiếm xa hơn trong chuỗi prototype.

Nếu bạn muốn thay đổi giá trị của một thuộc tính lúc thực thi và muốn giá trị mới đó áp dụng cho tất cả các thực thể được tạo ra, thì bạn không thể khởi tạo thuộc tính trong hàm dựng. Thay vào đó, ban thêm nó vào hàm vào trong thuộc tính prototype của hàm dựng. Ví dụ sau minh họa cách chúng ta đạt được mục đích này:

function Employee() {
  this.dept = 'general';    // Note that this.name (a local variable) does not appear here
}
Employee.prototype.name = '';    // A single copy

function WorkerBee() {
  this.projects = [];
}
WorkerBee.prototype = new Employee;

var amy = new WorkerBee;

Employee.prototype.name = 'Unknown';

Trong trường hợp này thuộc tính name của amy nhận giá trị "Unknown".

Tóm lại, nếu bạn muốn đặt giá trị mặc định cho thuộc tính và bạn muốn thay đổi giá trị mặc định lúc thực thi, bạn nên cài đặt thuộc tính trên prototype của hàm dựng, không phải tạo trực tiếp thuộc tính lúc thực thi hàm dựng.

Xác định mối quan hể của thực thể

Việc tìm kiếm thuộc tính trong JavaScript sẽ được thực hiện trên chính danh sách các thuộc tính riêng trực tiếp của object trước, nếu thuộc tính không được tìm thấy, nó sẽ tìm trên object đặc biệt là __proto__. Việc kiểm tra sẽ được thực thi đệ qui, qui trình này được gọi là "Tìm kiếm trong chuỗi prototype".

Thuộc tính đặc biệt __proto__ được tạo ra khi một đối tượng được tạo ra. Nó chính là giá trị được gán cho thuộc tính prototype của hàm dựng. Ví vậy biểu thức new Foo() tạo ra một object với __proto__ == Foo.prototype. Tiếp theo đó, những thay đổi trên thuộc tính Foo.prototype sẽ làm thay đổi thuộc tính cho tất cả đối tượng mà được tạo ra bởi new Foo().

Mỗi object đều cho thuộc tính __proto__ và mỗi hàm đều có thuộc tính prototype. Vì vậy những đối tượng có mối liên hệ thông qua cơ chế 'thừa kế prototype' với những đối tượng khác. Bạn có thể kiểm tra sự thừa kế bằng cách so sánh thuộc tính __proto__ của object với prototype của một hàm. JavaScript cung cấp một toán tử để thực hiện điều này là instanceof, Toán tử sẽ trả về kết quả true nếu đối tượng được kết thừa từ prototype của hàm. Như ví dụ sau:

var f = new Foo();
var isTrue = (f instanceof Foo);

Để minh họa cụ thể hơn, chúng ta xem ví dụ sau. Giả định bạn đã có đoạn mã như định nghĩa trong Kế thừa thuộc tính. Tạo một đối tượng Engineer như sau:

var chris = new Engineer('Pigman, Chris', ['jsd'], 'fiji');

Với đối tượng nay, những câu lệnh sau tất cả đều đúng:

chris.__proto__ == Engineer.prototype;
chris.__proto__.__proto__ == WorkerBee.prototype;
chris.__proto__.__proto__.__proto__ == Employee.prototype;
chris.__proto__.__proto__.__proto__.__proto__ == Object.prototype;
chris.__proto__.__proto__.__proto__.__proto__.__proto__ == null;

Dựa trên những điểm trên, bạn có thể viết hàm instanceOf như sau:

function instanceOf(object, constructor) {
   object = object.__proto__;
   while (object != null) {
      if (object == constructor.prototype)
         return true;
      if (typeof object == 'xml') {
        return constructor.prototype == XML.prototype;
      }
      object = object.__proto__;
   }
   return false;
}
Lưu Ý: Đoạn lệnh trên dùng toán tử typeof để kiểm tra object để kiểm tra đó là XML object, đây là cách khắc phục lỗi của JavaScript cho đối tượng XML.

Dùng hàm instanceOf trên, biểu thức luôn đúng:

instanceOf(chris, Engineer)
instanceOf(chris, WorkerBee)
instanceOf(chris, Employee)
instanceOf(chris, Object)

Nhưng biểu thức sau thì sai:

instanceOf(chris, SalesPerson)

Thông tin toàn cục trong hàm dựng

Khi bạn định nghĩa hàm dựng, bạn cần cẩn thận nếu bạn thiết lập giá trị cho biến toàn cục. Giả sử bạn muốn giá trị ID duy nhất được tự động gán cho mỗi employee mới. Bạn có thể sử dụng cách định nghĩa sau cho Employee sau:

var idCounter = 1;

function Employee(name, dept) {
   this.name = name || '';
   this.dept = dept || 'general';
   this.id = idCounter++;
}

Với định nghĩa này, khi bạn tạo một Employee mới, hàm dựng gán giá trị ID kế tiếp và rồi tăng biến đếm toàn cục ID lên một đơn vị. Vì vậy, những câu lệnh tiếp theo sau đây, victoria.id là 1 và harry.id là 2:

var victoria = new Employee('Pigbert, Victoria', 'pubs');
var harry = new Employee('Tschopik, Harry', 'sales');

Thoạt nhìn mọi thứ có vẽ tốt. Tuy nhiên, idCounter được tăng lên mỗi lần một đối tượng Employee được tạo ra cho mọi trường hợp cả những trường hợp không mong muốn. Nếu bạn tạo toàn bộ cây cấp bậc Employee như hình trong chương này, hàm dựng Employee được gọi mỗi là bạn cài đặt một prototype. Giả sử bạn có đoạn lệnh sau:

var idCounter = 1;

function Employee(name, dept) {
   this.name = name || '';
   this.dept = dept || 'general';
   this.id = idCounter++;
}

function Manager(name, dept, reports) {...}
Manager.prototype = new Employee;

function WorkerBee(name, dept, projs) {...}
WorkerBee.prototype = new Employee;

function Engineer(name, projs, mach) {...}
Engineer.prototype = new WorkerBee;

function SalesPerson(name, projs, quota) {...}
SalesPerson.prototype = new WorkerBee;

var mac = new Engineer('Wood, Mac');

Giả sử ta không quan tâm những câu lệnh bên trong hàm dựng, thuộc tính base và gọi hàm base trong chuỗi prototype. Trong trường hợp này, ngay khi đối tượng mac được tạo, mac.id là 5.

Tuy theo yêu cầu của ứng dụng, nó có thể là vấn đề quan trọng hoặc không khi biến đếm được tăng lên. Nếu bạn quan tâm tính chính xác của giá trị tăng thêm của biến đếm, Một giải pháp cho vấn đề này là:

function Employee(name, dept) {
   this.name = name || '';
   this.dept = dept || 'general';
   if (name)
      this.id = idCounter++;
}

Khi bạn tạo một thực thể của Employee để sử dụng như prototype, bạn không cung cấp đối số cho hàm dựng. Việc sử dụng định nghĩa của hàm dựng, khi bạn không cung cấp đối số, hàm dựng không gán giá trị cho id và không cập nhật biến đếm. Vì vậy, để một đối tượng Employee được gán id, bạn phải chỉ định tên cho employee. Trong ví dụ này, mac.id sẽ là 1.

Một cách khác, bạn có thể tạo một bản sao của đối tượng prototype của Employee để gán cho WorkerBee:

WorkerBee.prototype = Object.create(Employee.prototype);
// instead of WorkerBee.prototype = new Employee

Không hỗ trợ đa kế thừa

Một vài ngôn ngữ hướng đối tượng cho phép đa kế thừa. Nghĩa là một object có thể kế thừa những thuộc tính và giá rị từ những đối tượng cha không liên quan.

Việc kế thừa những thuộc tính xảy ra lúc thực thi bởi JavaScript trong khi tìm kiếm chuỗi prototype của object cho một giá trị. Bởi vì một object chỉ có một prototype đơn, JavaScript không thể kế thừa động từ nhiều hơn một chuỗi prototype.

Trong JavaScript, bạn có thể áp dụng nhiều hàm dựng trong hàm dựng. Điều này chứng minh tính khả thi của đa kế thừa. Xem xét ví dụ sau:

function Hobbyist(hobby) {
   this.hobby = hobby || 'scuba';
}

function Engineer(name, projs, mach, hobby) {
   this.base1 = WorkerBee;
   this.base1(name, 'engineering', projs);
   this.base2 = Hobbyist;
   this.base2(hobby);
   this.machine = mach || '';
}
Engineer.prototype = new WorkerBee;

var dennis = new Engineer('Doe, Dennis', ['collabra'], 'hugo');

Giả sử WorkerBee được định nghĩa như trên. Trong trường hợp này, đối tượng dennis có những thuộc tính sau:

dennis.name == 'Doe, Dennis';
dennis.dept == 'engineering';
dennis.projects == ['collabra'];
dennis.machine == 'hugo';
dennis.hobby == 'scuba';

Vì vậy dennis có thuộc tính từ hàm dựng Hobbyist. Tuy nhiên, giả sử sau đó bạn thêm một thuộc tính vào prototype của hàm dựng của Hobbyist:

Hobbyist.prototype.equipment = ['mask', 'fins', 'regulator', 'bcd'];

Đối tượng dennis không kế thừa thuộc tính mới này.