Appearance
Creating Objects
JavaScript sets a prototype for every created object, pointing to its prototype object.
When we access a property of an object using obj.xxx
, the JavaScript engine first searches the current object for that property. If it doesn't find it, it looks in the prototype object, continuing up to Object.prototype
. If still not found, it returns undefined
.
For example, creating an Array object:
javascript
let arr = [1, 2, 3];
Its prototype chain is:
null
▲
│
┌─────────────────┐
│Object.prototype │
└─────────────────┘
▲
│
┌─────────────────┐
│ Array.prototype │
└─────────────────┘
▲
│
┌─────────────────┐
│ arr │
└─────────────────┘
Array.prototype
defines methods like indexOf()
and shift()
, allowing direct calls on all Array objects.
When we create a function:
javascript
function foo() {
return 0;
}
The function also has a prototype chain:
null
▲
│
┌───────────────────┐
│ Object.prototype │
└───────────────────┘
▲
│
┌───────────────────┐
│Function.prototype │
└───────────────────┘
▲
│
┌───────────────────┐
│ foo │
└───────────────────┘
Function.prototype
defines methods like apply()
, so all functions can call apply()
.
Long prototype chains can slow down property access, so keep them manageable.
Constructor Functions
Besides creating objects directly with { ... }
, JavaScript can use constructor functions. First, define a constructor function:
javascript
function Student(name) {
this.name = name;
this.hello = function () {
alert('Hello, ' + this.name + '!');
}
}
You might wonder, isn't this just a regular function?
Indeed, it is a regular function, but in JavaScript, it can be called with the new
keyword to return an object:
javascript
let xiaoming = new Student('Ming');
xiaoming.name; // 'Ming'
xiaoming.hello(); // Hello, Ming!
Without new
, it’s a regular function returning undefined
. With new
, it acts as a constructor, binding this
to the newly created object and returning this
by default.
The prototype chain of the new xiaoming
is:
null
▲
│
┌─────────────────┐
│Object.prototype │
└─────────────────┘
▲
│
┌─────────────────┐
│Student.prototype│
└─────────────────┘
▲
│
┌─────────────────┐
│ xiaoming │
└─────────────────┘
This means xiaoming
's prototype points to the prototype of the Student
function. If you create xiaohong
and xiaojun
, their prototypes will be the same as xiaoming
's:
null
▲
│
┌─────────────────┐
│Object.prototype │
└─────────────────┘
▲
│
┌─────────────────┐
│Student.prototype│
└─────────────────┘
▲ ▲ ▲
│ │ │
┌─────────┐┌─────────┐┌─────────┐
│xiaoming ││xiaohong ││ xiaojun │
└─────────┘└─────────┘└─────────┘
Objects created with new Student()
also inherit a constructor
property from the prototype, pointing to the Student
function itself:
javascript
xiaoming.constructor === Student.prototype.constructor; // true
Student.prototype.constructor === Student; // true
Object.getPrototypeOf(xiaoming) === Student.prototype; // true
xiaoming instanceof Student; // true
The red arrows in the diagram represent the prototype chain. The object pointed to by Student.prototype
is the prototype of xiaoming
and xiaohong
, which also has a constructor
property pointing to the Student
function.
In addition, the Student
function has a prototype
property pointing to the prototype object of xiaoming
and xiaohong
, but these objects do not have a prototype
property; you can view it using the non-standard __proto__
.
Now we consider xiaoming
and xiaohong
to "inherit" from Student
.
A small observation:
javascript
xiaoming.name; // 'Ming'
xiaohong.name; // 'Hong'
xiaoming.hello; // function: Student.hello()
xiaohong.hello; // function: Student.hello()
xiaoming.hello === xiaohong.hello; // false
xiaoming
and xiaohong
have different names, which is correct. Otherwise, we couldn't differentiate them.
While both xiaoming
and xiaohong
have their own hello
function, they are two distinct functions, even though they share the same name and code.
If we create many objects with new Student()
, these objects' hello
functions could share the same instance to save memory.
To enable shared methods, we can move the hello
function to the shared prototype:
javascript
function Student(name) {
this.name = name;
}
Student.prototype.hello = function () {
alert('Hello, ' + this.name + '!');
};
Creating prototype-based JavaScript objects with new
is that simple!
Forgetting to Use new
If a function intended as a constructor is called without new
, what happens?
In strict mode, this.name = name
will throw an error because this
is undefined
. In non-strict mode, this.name = name
does not throw an error because this
refers to window
, unintentionally creating a global variable name
and returning undefined
, which is worse.
So, always remember to use new
with constructor functions. To distinguish between regular and constructor functions, it’s convention to capitalize constructor names and lowercase regular function names, helping syntax checkers like jslint catch forgotten new
.
Finally, we can write a createStudent()
function to encapsulate the new
operation. A common programming pattern looks like this:
javascript
function Student(props) {
this.name = props.name || 'anonymous'; // Default to 'anonymous'
this.grade = props.grade || 1; // Default to 1
}
Student.prototype.hello = function () {
alert('Hello, ' + this.name + '!');
};
function createStudent(props) {
return new Student(props || {});
}
This createStudent()
function has several advantages: it doesn't require new
, and it allows flexible parameters, which can be omitted or passed like this:
javascript
let xiaoming = createStudent({
name: 'Ming'
});
xiaoming.grade; // 1
If there are many properties for the created object, you only need to pass the desired ones; the rest can have default values. Since parameters are an object, we don’t need to remember their order. If we happen to have an object from JSON, we can directly create xiaoming
.
Exercise
Please define a Cat
constructor function that gives all Cat
objects a name
property and shares a method say()
, returning the string 'Hello, xxx!'
:
javascript
function Cat(name) {
// TODO:
}
// Test:
let kitty = new Cat('Kitty');
let doraemon = new Cat('Doraemon');
if (kitty && kitty.name === 'Kitty'
&& kitty.say
&& typeof kitty.say === 'function'
&& kitty.say() === 'Hello, Kitty!'
&& kitty.say === doraemon.say
) {
console.log('test passed!');
} else {
console.log('test failed!');
}