JavaScript 面向对象编程的基础知识篇 2 。
1. 概述
在上篇文章 JavaScript之new命令 中提到, this
可以用在构造函数之中,表示实例对象。
此外,this
还可以用在别的场合。但不管是什么场合,this
都有一个共同点:它总是返回一个对象。
简单说,this
就是属性或方法 “当前” 所在的对象。
下面是一个实际的例子。
var person = {
name: '张三',
describe: function () {
return '姓名:'+ this.name;
}
};
person.describe()
// "姓名:张三"
上面代码中,this.name
表示 name
属性所在的那个对象。由于this.name
是在 describe
方法中调用,而 describe
方法所在的当前对象是person
,因此 this
指向 person
,this.name
就是person.name
。
只要函数被赋给另一个变量,this
的指向就会变。
var A = {
name: '张三',
describe: function () {
return '姓名:'+ this.name;
}
};
var name = '李四';
var f = A.describe;
f() // "姓名:李四"
上面代码中,A.describe
被赋值给变量f
,内部的this
就会指向f
运行时所在的对象(本例是顶层对象)。
2. this 的实质
var obj = { foo: 5 };
上面的代码将一个对象赋值给变量 obj
。JavaScript 引擎会先在内存里面,生成一个对象 { foo: 5 }
,然后把这个对象的内存地址赋值给变量 obj
。也就是说,变量obj
是一个地址(reference)。后面如果要读取 obj.foo
,引擎先从obj
拿到内存地址,然后再从该地址读出原始的对象,返回它的foo
属性。
由于函数可以在不同的运行环境执行,所以需要有一种机制,能够在函数体内部获得当前的运行环境(context)。所以,this
就出现了,它的设计目的就是在函数体内部,指代函数当前的运行环境。
var f = function () {
console.log(this.x);
}
上面代码中,函数体里面的 this.x
就是指当前运行环境的 x
。
3. 使用场合
this
主要有以下几个使用场合。
(1)全局环境
全局环境使用 this
,它指的就是顶层对象 window
。
this === window // true
function f() {
console.log(this === window);
}
f() // true
上面代码说明,不管是不是在函数内部,只要是在全局环境下运行,this
就是指顶层对象window
。
(2)构造函数
构造函数中的 this
,指的是实例对象。
var Obj = function (p) {
this.p = p;
};
上面代码定义了一个构造函数 Obj
。由于this
指向实例对象,所以在构造函数内部定义this.p
,就相当于定义实例对象有一个 p
属性。
(3)对象的方法
如果对象的方法里面包含 this
,this
的指向就是方法运行时所在的对象。该方法赋值给另一个对象,就会改变 this
的指向。
4. 使用注意点
4.1 避免多层 this
由于 this
的指向是不确定的,所以切勿在函数中包含多层的 this
。
var o = {
f1: function () {
console.log(this);
var f2 = function () {
console.log(this);
}();
}
}
o.f1()
// Object
// Window
上面代码包含两层 this
,结果运行后,第一层指向对象 o
,第二层指向全局对象,因为实际执行的是下面的代码。
var temp = function () {
console.log(this);
};
var o = {
f1: function () {
console.log(this);
var f2 = temp();
}
}
事实上,使用一个变量固定 this
的值,然后内层函数调用这个变量,是非常常见的做法,请务必掌握。
JavaScript 提供了严格模式,也可以硬性避免这种问题。严格模式下,如果函数内部的 this
指向顶层对象,就会报错。
var counter = {
count: 0
};
counter.inc = function () {
'use strict';
this.count++
};
var f = counter.inc;
f()
// TypeError: Cannot read property 'count' of undefined
上面代码中,inc
方法通过 'use strict'
声明采用严格模式,这时内部的this
一旦指向顶层对象,就会报错。
4.2 避免数组处理方法中的 this
数组的 map
和 foreach
方法,允许提供一个函数作为参数。这个函数内部不应该使用 this
。
4.3 避免回调函数中的 this
回调函数中的 this
往往会改变指向,最好避免使用。
可以采用下面的一些方法对 this
进行绑定,就是使得 this
固定指向某个对象,减少不确定性。
5. 绑定 this 的方法
JavaScript 提供了 call
、apply
、bind
这三个方法,来切换/固定 this
的指向。
5.1 Function.prototype.call()
函数实例的 call
方法,可以指定函数内部this
的指向(即函数执行时所在的作用域),然后在所指定的作用域中,调用该函数。
call
方法的参数,应该是一个对象。如果参数为空、null
和undefined
,则默认传入全局对象。
var n = 123;
var obj = { n: 456 };
function a() {
console.log(this.n);
}
a.call() // 123
a.call(null) // 123
a.call(undefined) // 123
a.call(window) // 123
a.call(obj) // 456
上面代码中,a
函数中的 this
关键字,如果指向全局对象,返回结果为123
。如果使用call
方法将 this
关键字指向 obj
对象,返回结果为456
。
可以看到,如果call
方法没有参数,或者参数为null
或undefined
,则等同于指向全局对象。
call
方法还可以接受多个参数。
func.call(thisValue, arg1, arg2, ...)
call
的第一个参数就是 this
所要指向的那个对象,后面的参数则是函数调用时所需的参数。
function add(a, b) {
return a + b;
}
add.call(this, 1, 2) // 3
上面代码中,call
方法指定函数 add
内部的 this
绑定当前环境(对象),并且参数为 1
和 2
,因此函数 add
运行后得到 3
。
5.2 Function.prototype.apply()
5.2.1 方法
apply
方法的作用与 call
方法类似,也是改变 this
指向,然后再调用该函数。唯一的区别就是,它接收一个数组作为函数执行时的参数,使用格式如下。
func.apply(thisValue, [arg1, arg2, ...])
apply
方法的第一个参数也是 this
所要指向的那个对象,如果设为null
或 undefined
,则等同于指定全局对象。
第二个参数则是一个数组,该数组的所有成员依次作为参数,传入原函数。
原函数的参数,在call
方法中必须一个个添加,但是在 apply
方法中,必须以数组形式添加。
function f(x, y){
console.log(x + y);
}
f.call(null, 1, 1) // 2
f.apply(null, [1, 1]) // 2
5.2.2 应用
(1)找出数组最大元素
JavaScript 不提供找出数组最大元素的函数。
结合使用apply
方法和Math.max
方法,就可以返回数组的最大元素。
var a = [10, 2, 4, 15, 9];
Math.max.apply(null, a) // 15
(2)将数组的空元素变为 undefined
通过 apply
方法,利用 Array
构造函数将数组的空元素变成undefined
。
Array.apply(null, ['a', ,'b'])
// [ 'a', undefined, 'b' ]
空元素与 undefined
的差别在于,数组的forEach
方法会跳过空元素,但是不会跳过 undefined
。
var a = ['a', , 'b'];
function print(i) {
console.log(i);
}
a.forEach(print)
// a
// b
Array.apply(null, a).forEach(print)
// a
// undefined
// b
(3)转换类似数组的对象
利用数组对象的 slice
方法,可以将一个类似数组的对象(比如arguments
对象)转为真正的数组。
(4)绑定回调函数的对象
由于 apply
方法(或者 call
方法)不仅绑定函数执行时所在的对象,还会立即执行函数,因此不得不把绑定语句写在一个函数体内。
5.3 Function.prototype.bind()
5.3.1 方法
bind
方法用于将函数体内的 this
绑定到某个对象,然后返回一个新函数。
bind
方法的参数就是所要绑定 this
的对象。
var counter = {
count: 0,
inc: function () {
this.count++;
}
};
var func = counter.inc.bind(counter);
func();
counter.count // 1
上面代码中,counter.inc
方法被赋值给变量func
。这时必须用bind
方法将inc
内部的this
,绑定到counter
,否则就会出错。
如果bind
方法的第一个参数是null
或 undefined
,等于将this
绑定到全局对象,函数运行时this
指向顶层对象(浏览器为window
)。
function add(x, y) {
return x + y;
}
var plus5 = add.bind(null, 5);
plus5(10) // 15
上面代码中,函数add
内部并没有this
,使用bind
方法的主要目的是绑定参数x
,以后每次运行新函数 plus5
,就只需要提供另一个参数 y
就够了。
5.3.2 使用注意点
(1)每一次返回一个新函数
bind
方法每运行一次,就返回一个新函数,这会产生一些问题。
element.addEventListener('click', o.m.bind(o));
element.removeEventListener('click', o.m.bind(o)); // 无法取消绑定
//正确的写法:
var listener = o.m.bind(o);
element.addEventListener('click', listener);
// ...
element.removeEventListener('click', listener);
(2)结合回调函数使用
var counter = {
count: 0,
inc: function () {
'use strict';
this.count++;
}
};
function callIt(callback) {
callback();
}
callIt(counter.inc.bind(counter));
counter.count // 1
上面代码中,callIt
方法会调用回调函数。这时如果直接把counter.inc
传入,调用时counter.inc
内部的 this
就会指向全局对象。使用 bind
方法将counter.inc
绑定counter
以后,就不会有这个问题,this
总是指向counter
。
(3)结合call方法使用
[1, 2, 3].slice(0, 1) // [1]
// 等同于
Array.prototype.slice.call([1, 2, 3], 0, 1) // [1]
上面的代码中,数组的slice
方法从[1, 2, 3]
里面,按照指定位置和长度切分出另一个数组。这样做的本质是在[1, 2, 3]
上面调用Array.prototype.slice
方法,因此可以用call
方法表达这个过程,得到同样的结果。
call
方法实质上是调用Function.prototype.call
方法,因此上面的表达式可以用bind
方法改写。
var slice = Function.prototype.call.bind(Array.prototype.slice);
slice([1, 2, 3], 0, 1) // [1]
上面代码的含义就是,将Array.prototype.slice
变成 Function.prototype.call
方法所在的对象,调用时就变成了Array.prototype.slice.call
。
参考链接
- 阮一峰, JavaScript 教程
网友评论