原文地址:https://wangdoc.com/javascript/
涵义
前一张提到this可以用在构造函数之中,表示实例对象。除此之外,this还可以用在别的场合。但是不管是什么场合,this都有一个共同点:它总是返回一个对象。
简单的说,this就是属性或方法当前所在的对象。由于对象的属性可以赋给另一个对象。所以属性所在的当前对象是可变的,即this的指向是可变的。
var A = {
name: "张三",
describe: function () {
return "姓名:" + this.name;
}
};
var B = {
name = "李四"
};
B.describe = A.describe;
B.describe() // "姓名:李四"
只要函数被赋值给另一个变量,this的指向就会变。
var A = {
name: "张三",
describe: function () {
return "姓名:" + this.name;
}
};
var name = "李四";
var f = A.describe;
f() // "姓名:李四"
实质
JavaScript语言之所以有this的涉及,跟内存里面的数据结构有关系。
var obj = {foo: 5};
上面的代码将一个对象赋值给变量obj。JavaScript引擎现在内存里面生成一个对象{foo:5},然后把这个对象的内存地址赋值给变量obj。也就是说,变量obj是一个地址(reference)。
原始的对象以字典结构保存,每一个属性名都对应一个属性描述对象。
{
foo: {
[[value]]: 5,
[[writable]]: true,
[[enumerable]]: true,
[[configurable]]: true
}
}
注意,foo属性的值保存在属性描述对象的value属性里面。
这样的结构是很清晰的,问题在于属性的值可能是一个函数。
var obj = { foo: function () {} };
这时,引擎会将函数单独保存在内存中,然后再将函数的地址赋给foo属性的value属性。由于函数是一个单独的值,所以它可以在不同的环境(上下文)执行。
JavaScript允许在函数体内部,引用当前环境的其他变量。所以需要一种机制,能够在函数体内部获取当前运行环境。所以this就出现了。
使用场合
this主要有以下几个使用场合。
(1)全局环境
全局环境使用this,它指的就是顶层对象window。
this === window // true
function f() {
console.log(this === window);
}
f() // true
(2)构造函数
构造函数中的this,指的是实例对象。
(3)对象的方法
如果对象的方法里面包含this,this指向的就是方法运行时所在的对象。该方法赋值给另一个对象,就会改变this的指向。
var obj = {
foo: function () {
console.log(this);
}
};
obj.foo() // obj
// 注意
(obj.foo = obj.foo)() // window
(false || obj.foo)() // window
(1, obj.foo)() // window
如果this所在的方法不在对象的第一层,这时this只是指向当前一层的对象,而不会继承跟上面的层。
var a = {
p: "hello",
b: {
m: function () {
console.log(this.p);
}
}
};
a.b.m() // undefined
如果这是将嵌套对象内部的方法赋值给一个变量,this依然会指向全局对象。
var hello = a.b.m;
hello() // 在window下运行
// 如果需要在引用b对象
var hello = a.b;
hello.m();
使用注意点
避免多层this
由于this的指向是不确定的,所以切勿在函数中包含多层的this。
var o = {
f1: function () {
console.log(this);
var f2 = function () {
console.log(this);
} ();
}
};
o.f1()
// object
// window
解决方案
var o = {
f1: function () {
console.log(this);
var that = this;
var f2 = function () {
console.log(that);
} ();
}
};
o.f1()
// object
// object
避免数组处理方法中的this
数组的map和foreach方法,允许提供一个函数作为参数。这个函数内部应该使用this。
var o = {
v: "hello",
p: ["a1", "a2"],
f: function () {
this.p.forEach(function (item) {
console.log(this.v + " " + item);
});
}
};
o.f()
// undefined a1
// undefined a2
上面代码中,forEach方法回调函数中的this,其实指向window对象,因此取不到o.v的值。解决这个问题的一种方法,使用中间变量固定this;另一个方法是将this当做forEach方法的第二个参数,固定它的运行环境。
避免回调函数中的this
回调函数中的this往往改变指向,最好避免使用。
绑定this的方法
this的动态切换,固然伟JavaScript创造了巨大的灵活性,但也使得编程变得困难和模糊。有时需要把this固定下来,避免出现意想不到的情况。JavaScript提供了call、apply、bind这三个方法来切换/固定this的指向。
Function.prototype.call()
函数实例的call方法,可以指定函数内部this的指向(即函数执行时所在的作用域),然后在所指定的作用域中,调用该函数。
var obj = {};
var f = function () {
return this;
};
f() === window // true
f.call(obj) === obj // true
call方法的参数应该是一个对象。如果参数为空、null和undefined,则默认传入全局对象。
如果call方法的参数是一个原始值,那么这个原始值会自动转成对应的包装对象,然后传入call方法。
var f = function () {
return this;
};
f.call(5) // Number {[[PromitiveValue]]: 5}
call方法还可以接受多个参数。
call的第一个参数就是this所要指向的那个对象,后面的参数则是函数调用时所需的参数。
function add(a, b) {
return a + b;
}
add.call(this, 1, 2) // 3
call方法的一个应用是调用对象的原生方法。
var obj = {};
obj.hasOwnProperty("toString"); // false
// 覆盖掉继承的 hasOwnProperty方法
obj.hasOwnProperty = function () {
return true;
};
obj.hasOwnProperty("toString") // true
Object.prototype.hasOwnProperty.call(obj, "toString") // false
Function.prototype.apply()
apply方法的作用与call方法类似,也是改变this指向,然后再调用该函数。唯一的区别就是,它是接受一个数组作为函数执行时的参数。
apply第一个参数也是this所要指向的对象,如果设为null或undefined,则等同于指定全局对象。
function f(x, y) {
console.log(x + y);
}
f.call(null, 1, 1) // 2
f.apply(null, [1, 1]) // 2
利用这一点可以做一些有趣的应用:
(1)找出数组最大元素
var a = [10, 2, 4, 15, 9];
Math.max.apply(null, a) // 10
(2)将数组的空元素变为undefined
通过apply方法,利用Array构造函数。
Array.apply(null, ["a", , "b"])
// ["a", undefined, "b"]
空元素与undefined的区别在于,数组的forEach方法会跳过空元素。
(3)转换类似数组对象
利用数组对象的slice方法,可以将一个类似数组的对象转为真正的数组。
Array.prototype.slice.apply({0: 1, length: 1}) // [1]
Array.prototype.slice.apply({0: 1}) // []
Array.prototype.slice.apply({0: 1, length: 2}) // [1, undefined]
Array.prototype.slice.apply({length: 1}) // [undefined]
(4)绑定回调函数
var o = new Object();
o.f = function () {
console.log(this === o);
};
var f = function () {
o.f.apply(o);
};
// JQuery 写法
$("#button").on("click", f);
Function.prototype.bind()
bind方法用于将函数体内的this绑定到某个对象,然后返回一个新的函数。
var d = new Date()
d.getTime() //
var print = d.getTime;
print() // Uncaught TypeError: this is not a Date object.
上面代码中,我们将d.getTime方法赋值给变量print,然后调用print就报错了。因为getTime方法内部的this,绑定Date对象的实例,赋给变量print以后,内部的this已经不指向Date对象的实例了。
bind方法可以解决这个问题。
var print = d.getTime.bind(d);
print();
bind方法的参数就是所要绑定的this对象。
var counter = {
count: 0,
inc: function () {
this.count++;
}
};
var func = counter.inc.bind(counter);
func();
counter.count // 1
因为成员方法中使用了this,指向counter对象的实例。所以需要使用bind来绑定this到counter。
bind还可以接受更多参数,将这些参数绑定原函数的参数。
var add = function (x, y) {
return x * this.m + y * this.n;
};
var obj = {
m: 2,
n: 2
};
var newAdd = add.bind(obj, 5);
newAdd(5) // 20
上面代码中bind方法除了绑定this对象,还将add函数的第一个参数x绑定为5,然后返回一个新的函数newAdd,这个函数只需要在接受一个参数y就能运行了。
如果bind方法的第一个参数是null或undefined,等于将this绑定到全局对象,函数运行时this指向顶层对象。
bind方法有一些使用注意点。
(1)每一次返回一个新函数
bind方法每运行一次,就会返回一个新的函数,这会产生一些问题。比如,监听事件的时候不能写成下面这样。
element.addEventListener("click", o.m.bind(o));
上面代码,click事件绑定bind方法生成的一个匿名函数。这样会导致无法取消绑定。
element.removeEventListener("click", o.m.bind(o));
正确的写法是:
var listener = o.m.bind(o);
element.addEventListener("click", listener);
element.removeEventListener("click", listener);
(2)结合回调函数使用
回调函数是JavaScript最常用的模式之一,但是很常见的一个错误是,将包含this的方法直接当做回调函数。解决方法是使用bind方法。
var obj = {
name: "hello",
times: [1, 2, 3],
print: function () {
this.times.forEach(function (n) {
console.log(this.name);
});
}
};
obj.print() // 没有输出
上面代码中回调函数的this指向全局对象。解决这个问题也是通过bind方法绑定this。
obj.print = function () {
this.times.forEach(function (n) {
console.log(this.name);
}.bind(this));
};
obj.print() // hello
(3)结合call方法使用
利用bind方法,可以改写一些JavaScript原生方法的使用形式,以数组的slice方法为例。
[1, 2, 3].slice(0, 1) // [1]
// 相当于
Array.prototype.slice.call([1, 2, 3], 0, 1) // [1]
call方法实质上是调用Function.prototype.call方法,因此上面的表达式可以用bind方法改写。
var slice = Function.prototype.call.bind(Array.prototype.slice);
slice([1, 2, 3], 0, 1) // [1]
如果再进一步,将Function.prototype.call方法绑定到Function.prototype.bind对象,就意味着bind的调用形式也可以被改写。
function f () {
console.log(this.v);
}
var o = { v: 123 };
var bind = Function.prototype.call.bind(Function.prototype.bind);
bind(f, o)() // 123
第一个参数,函数实例,第二个参数this指向的对象。