Appearance
Method
Binding a function within an object is called a method of that object.
In JavaScript, an object is defined like this:
javascript
let xiaoming = {
name: 'Ming',
birth: 1990
};
However, if we bind a function to xiaoming, we can do more. For example, writing an age()
method that returns xiaoming's age:
javascript
let xiaoming = {
name: 'Ming',
birth: 1990,
age: function () {
let y = new Date().getFullYear();
return y - this.birth;
}
};
xiaoming.age; // function xiaoming.age()
xiaoming.age(); // This year it's 25; next year it will be 26.
A function bound to an object is called a method. It’s similar to a regular function, but it uses a special variable called this
internally. What is this
?
Inside a method, this
is a special variable that always points to the current object, which in this case is the variable xiaoming. Thus, this.birth
accesses xiaoming's birth
property.
Let’s break it down:
javascript
function getAge() {
let y = new Date().getFullYear();
return y - this.birth;
}
let xiaoming = {
name: 'Ming',
birth: 1990,
age: getAge
};
xiaoming.age(); // 25, normal result
getAge(); // NaN
Why does calling the function getAge()
return NaN? Notice we’ve entered a significant pitfall in JavaScript.
When a function calls this
, to whom does it refer?
The answer is: it depends on the context!
If called as a method of an object, like xiaoming.age()
, this
points to the calling object, xiaoming, which aligns with our expectations.
If called independently, like getAge()
, then this
points to the global object, which is window
.
What a mess!
Even worse is when you do this:
javascript
let fn = xiaoming.age; // Getting xiaoming's age function
fn(); // NaN
This also fails! To ensure this
points correctly, you must use the obj.xxx()
format!
Due to this significant design flaw, fixing it isn’t straightforward. ECMA decided that in strict mode, a function's this
should point to undefined
. Therefore, in strict mode, you will encounter an error:
javascript
'use strict';
let xiaoming = {
name: 'Ming',
birth: 1990,
age: function () {
let y = new Date().getFullYear();
return y - this.birth;
}
};
let fn = xiaoming.age;
fn(); // Uncaught TypeError: Cannot read property 'birth' of undefined
This decision only exposes the error but doesn’t solve the issue of where this
should point.
Sometimes, if you, as a refactoring enthusiast, restructure the method:
javascript
'use strict';
let xiaoming = {
name: 'Ming',
birth: 1990,
age: function () {
function getAgeFromBirth() {
let y = new Date().getFullYear();
return y - this.birth;
}
return getAgeFromBirth();
}
};
xiaoming.age(); // Uncaught TypeError: Cannot read property 'birth' of undefined
The result again throws an error! The reason is that the this
pointer only points to xiaoming within the age
method. The function defined inside has its this
pointing to undefined
(or the global object in non-strict mode).
There is a solution: capture this
with a that
variable:
javascript
'use strict';
let xiaoming = {
name: 'Ming',
birth: 1990,
age: function () {
let that = this; // Capture this at the beginning of the method
function getAgeFromBirth() {
let y = new Date().getFullYear();
return y - that.birth; // Use that instead of this
}
return getAgeFromBirth();
}
};
xiaoming.age(); // 25
Using let that = this;
allows you to safely define other functions within the method without cluttering everything into one method.
Apply
Although in an independent function call, this
points to undefined
or window
depending on strict mode, we can still control what this
refers to!
To specify which object a function's this
points to, you can use the function's apply
method. It takes two parameters: the first is the this
value you want to bind, and the second is an array representing the function's parameters.
Use apply
to fix the getAge()
call:
javascript
function getAge() {
let y = new Date().getFullYear();
return y - this.birth;
}
let xiaoming = {
name: 'Ming',
birth: 1990,
age: getAge
};
xiaoming.age(); // 25
getAge.apply(xiaoming, []); // 25, this points to xiaoming, parameters empty
Another method similar to apply()
is call()
, with the only difference being:
apply()
packs parameters into an array before passing them;call()
passes parameters individually.
For example, calling Math.max(3, 5, 4)
can be achieved with apply()
and call()
as follows:
javascript
Math.max.apply(null, [3, 5, 4]); // 5
Math.max.call(null, 3, 5, 4); // 5
For ordinary function calls, we typically bind this
to null
.
Decorators
By utilizing apply()
, we can also dynamically change a function's behavior.
All JavaScript objects are dynamic, meaning we can even reassign built-in functions.
Now assume we want to track how many times parseInt()
has been called. We could manually count each call, but that’s inefficient. The best approach is to replace the default parseInt()
with our own function:
javascript
'use strict';
let count = 0;
let oldParseInt = parseInt; // Save the original function
window.parseInt = function () {
count += 1;
return oldParseInt.apply(null, arguments); // Call the original function
};
// Test:
parseInt('10');
parseInt('20');
parseInt('30');
console.log('count = ' + count); // 3