1. 关于This
相比在Java中this
只指代当前类的用法,Javascript中this
的并不是一个固定的指代,其指代存在绑定规则
1.1 使用原因
其实大部分情况下,使用词法作用域已经可以解决所有需要使用this
的场景,代码中完全可以不使用this
。但是使用this
可以带来函数调用过程中对于函数参数传递的优化。举个例子:
function foo(a, b){
console.log(a + b);
}
f(1, 2); // 3
在函数调用过程中需要传递两个参数进行调用,但是如果参数是某个对象中的某些属性,那么使用这种调用方式就变成如下情况:
var obj = {a: 1 ,b: 2};
foo(obj.a, obj.b); // 3
在传递参数的时候明显比较麻烦,尤其在于如果要利用对象中的复数属性的时候,参数传递就变得特别多,这个时候使用this
就可以进行简化:
function foo() {
console.log(this.a + this.b);
}
var obj = {a: 1, b: 2};
foo.apply(obj); // 3
当然我们完全可以将obj
作为参数进行传递,所以说使用this
并非开发中必须。
1.2 绑定要点
this
到底代表什么关键在于this
的绑定规则,但无论什么规则,首先要明确两点:第一,this
不代表自身,第二,this
和词法作用域没有必然联系例:
function foo() {
console.log(this.a); // 这里的this并不指代foo函数本身,也和词法作用域无关
}
以上两点从根本上来说指在函数进行声明的时候,并不能确定this
代表什么,只有在函数调用的时候,this
的绑定关系才可以确定(就像动态作用域一样)
this
只有在函数调用的时候确定绑定关系
this
只有在函数调用的时候确定绑定关系
this
只有在函数调用的时候确定绑定关系
重要的事情说三遍,不过再补充一下:
箭头函数是特别的
箭头函数是特别的
箭头函数是特别的
2. 绑定规则
根据Javascript中的用法,this
有以下的几种绑定规则:
2.1 全局绑定
直接在Javascript全局中使用this
,这个时候this
指向一个全局对象,在浏览器中代表window
对象,node中代表global
对象:
console.log(this);
2.2 Function Invoke(默认绑定)
直接调用方法的时候,如果在严格模式下,this
的绑定为undefined
(之所以返回undefined
,是因为函数调用的时候执行的上下文并不确定,严格模式于是将执行中的this
绑定为undefined
),非严格模式下,this
绑定为全局对象,同 2.1 全局绑定 :
// 1. 非严格模式下
function foo(){
console.log(this);
}
foo(); // window
// 2. 严格模式下
(function(){
'use strict'
function foo() {
console.log(this);
}
foo(); // undefined
})()
// 3. 严格模式下调用
(function(){
'use strict'
foo(); // window
})()
请注意例子中的第三个调用,在调用的时候使用了严格模式,但是打印结果并不是undefined
,因此说明严格模式是在函数声明的时候对this
的绑定产生影响,在调用的时候对此并没有影响。
如果之后所有的规则都不符合的时候,将使用这一条规则,来进行this
的绑定。
2.3 Method Invoke(隐式绑定)
当使用对象调用的时候,this
将绑定为该对象:
var obj = {
a: 1,
foo: function() {
console.log(this.a);
}
}
obj.foo(); // 1
但是隐式绑定过程存在两种隐式丢失的情况(可以使用箭头函数解决,详见 2.6 箭头函数),使用函数别名和回调函数:
var obj = {
a: 1,
foo: function() {
console.log(this.a);
}
}
// 1. 使用函数别名
var baz = obj.foo;
baz(); // undefined
// 2. 使用回调函数
setTimeout(obj.foo , 0); // undefined
原因在于在使用函数别名和回调函数的时候,可以理解为进行了以下的操作
var baz = obj.foo = function() {
console.log(this.a);
}
于是,在调用baz
的时候,将根据 2.2 默认绑定 对this
进行处理
2.4 apply, call, bind (显示绑定)
使用显示绑定的时候,this
绑定为传入的对象:
function foo(a, b, c){
console.log(this.a + a + b + c);
}
foo.apply({a: 1}, [1, 2, 3]) // 1 + 1 + 2 + 3 = 7
foo.call({a: 1}, 1, 2, 3) // 7
foo.bind({a: 1})(1, 2, 3) // 7
apply
和call
的使用本质上没有区别,只是传递参数不同
bind
是ES5之后Function
原型链上增加的方法,先将this
绑定到传递的对象,但是使用bind
进行绑定的时候是硬绑定,硬绑定是指这个绑定只能绑定一次,只有第一次绑定可以生效:
function foo() {
console.log(this.a);
}
foo.bind({a: 1}).bind({a: 2})(); // 1
因为现在很多内置对象提供了原型链上的方法,于是通过这种原型链上API的调用是显示调用的过程
function foo(i) {
console.log(this.a + i);
}
[1,2,3].forEach(foo, {a: 1}); // 2 3 4
2.5 new(构造调用)
使用new
进行绑定的时候,this
绑定为构建函数构造的新对象,例:
function F() {
this.a = 1;
}
var f = new F(); // 等同于 var f = new F;
console.log(f.a); // 1
这里使用new
进行构造的时候,Javascript会按照以下的过程执行
创建一个新对象 --> 将对象和构造方法的prototype
做关联 --> 将this
绑定到该对象 --> 如果构造函数无返回值则返回该对象,否则返回新对象
所以,转换为代码理解也就是:
// 1. 无返回的构造函数
function F() {
this.a = 1;
}
/* 使用new以后,函数内部变化为
function F() {
var _o_ = {} ; // 创建一个新对象
_o_.prototpye = F.prototype; // 原型链关联
_o_.a = 1; // this绑定到对象
return _o_; // 返回该对象
}
*/
var f = new F();
console.log(f.a); // 1
// 2. 有返回的构造函数
function F() {
this.a = 1;
return {b: 2};
}
/* 使用new以后,函数内部变化为
function F() {
var _o_ = {} ; // 创建一个新对象
_o_.prototpye = F.prototype; // 原型链关联
_o_.a = 1; // this绑定到对象
return {b: 2}; // 返回原返回值
}
*/
var f = new F();
console.log(f.a) // undefined
console.log(f.b) // 2
于是,如果原构造函数存在返回值的情况下,this
绑定为了函数内部创建的新对象,但是这个新创建的对象并没有返回,所以没办法再对这个绑定的内容进行引用了。
2.6 箭头函数
使用箭头函数进行绑定的时候,this
绑定外层(函数或者全局)作用域环境:
var obj = {
a: 1,
foo: () => {
console.log(this);
}
}
obj.foo(); // window
这个例子看上去和 2.3 隐式绑定 是差不多的,但是区别这里的this
指向发生变化,this不再指向调用对象本身,而是直接使用外层函数全局对象。
但是下面的例子是需要注意的:
function f(){
return function() {
console.log(this.a);
}
}
f.apply({a:1}).apply({a:2}) // 2;
// 箭头函数
function f(){
return() => {
console.log(this.a);
}
}
f.apply({a:1}).apply({a:2}) // 1
在第一次调用 apply
的时候,生成了一个外层的作用域,此时this
指向了该外层全局对象的{a: 1}
,之后不会再发生变化
2.7 事件绑定
使用事件函数进行绑定的时候,根据使用情况不同,this
通常指向当前事件节点,例:
// 1. 使用addEventListener等事件绑定,this指向绑定的节点
// 2. 使用html事件绑定,this指向当前dom节点
// 3. 使用html事件绑定,增加IIFE,this指向window
3. 优先级
当我们如果使用的情况比较复杂,同时存在以上几种绑定关系的时候,我们该如何处理?实际上绑定的关系存在一个优先级,按照优先级来进行处理(这里除开 2.7 事件绑定,因为通常事件绑定下不会存在那么复杂的关系)
2.5 构造绑定 > 2.4 显示绑定 > 2.3 隐式绑定 > 2.2 默认绑定
2.6 箭头函数 有特殊表现
例:
// 1. 隐式绑定 > 默认绑定
function foo() {
console.log(this.a);
}
var obj = {a: 1, foo: foo};
obj.foo(); // 1
// 2. 显示绑定 > 隐式绑定
obj.foo.apply({a: 2}) // 2
// 3. 构造绑定 > 显示绑定
function F(a) {
this.a = a;
}
var obj = {};
var bar = F.bind(obj);
bar(1);
console.log(obj.a); // 1
var obj2 = new bar(2);// 使用new修改了this的绑定
console.log(obj.a); // 1 原来的值没有发生变化
console.log(obj2.a); // 2 新的值发生了变化
// 4. 箭头函数的特殊表现
var a = 2;
var f = () => {
console.log(this.a);
}
f.apply({a: 3}); // 2
4. 其他
4.1 显示绑定中null
的使用
在进行显示绑定的时候,如果传递绑定对象为null
时,因为不符合我们说的其他规则,所以将使用默认规则:
function foo() {
console.log(this);
}
foo.apply(null) // window
但是如果直接传递null
进行方法调用存在变量泄漏的风险:
function foo() {
this.a = 1;
}
foo.apply(null);
console.log(a); // 1
4.2 Object.create(null)
为了解决 4.1 显示绑定中null的使用 存在的变量泄漏风险,于是可以使用Object.create(null)
创建一个空对象,使用这种方式创建的空对象,比{}
更纯粹,因为他不继承Object.prototype
function foo() {
this.a = 1;
}
foo.apply(Object.create(null));
console.log(a); // ReferenceError
5. 参考
《你不知道的Javascript(上卷)》
MDN this
Gentle explanation of 'this' keyword in JavaScript
Function.prototype.call
Function.prototype.apply
Function.prototype.bind
6. 练习
惯例最后来个练习吧
function foo() {
console.log(this.a);
}
var obj = {
a: 1,
foo: foo
}
var obj2 = {
a: 1,
foo: () => {
console.log(this.a);
}
}
function F() {
this.a = 2;
}
var f = new F();
foo(); // undefined
obj.foo(); // 1
obj.foo.bind({a: 2})(); // 2
obj2.foo.bind({a: 2})(); // undefined
console.log(f.a); // 2