关于this
this既不是指向函数自身也不是指向函数的词法作用域,this实际上是在函数被调用时发生的绑定,
它指向什么完全取决于函数在哪里被调用。
this的调用位置
所谓的调用位置就是函数在代码中被调用的位置,可以通过分析调用栈来找到它的调用位置。
this的绑定规则
默认绑定
默认的this是指向全局对象的
function foo(){
console.log(this.a)
}
var a=2;
foo() //输出2
上面的函数foo是在全局作用域声明的,变量a也是,foo()的调用位置很明显是在全局作用域里。
这里的this指向的实际是全局对象。
注意:严格模式下,全局对象无法被默认绑定,所以this会绑定到undefined。
隐式绑定
当函数引用有上下文对象的时候,隐式规则会把函数调用中的this绑定到上下文对象。
应用该规则首先要考虑的是调用位置是否存在上下文对象。简单说是否包含在某个对象里。
function foo(){
console.log(this.a)
}
var obj = {
a:2,
foo:foo
}
obj.foo() //2
这里foo被当作引用属性添加到了obj对象里,但是严格意义上来说这个函数不属于obj对象,即使它变为了obj的一个属性值,
但是后面调用的时候使用了obj这个上下文来引用foo函数,所以可以理解成,函数被调用时obj对象包含它。(强调调用时)
因为是通过obj对象调用foo的,所以这里的this指向是obj(上下文对象)。
如果存在多层对象属性引用链,根据最后一层对象确定this的指向。
function foo(){
console.log(this.x)
}
var b = {
x:3
foo:foo
}
var a= {
x:5
b:b
}
a.b.foo() //3
在这个调用链的最外层是b,所以this指向的是b,进而得知this.x就是b.x等于3。
还有一种情况就是把函数作为参数作为传递
function foo(){
console.log(this.a)
}
function doFoo(fn){
// a =4
//接收参数等价于对fn赋值
fn(); // 调用位置
}
var obj = {
a:2,
foo:foo
};
var a = "oops,gloal";//a是全局对象的属性,nodejs下去掉var就行。
doFoo(obj.foo); //输出oops,gloal
为什么输出的内容不是2呢,这里就设置的隐式赋值的问题,函数doFoo被调用的时候
传入参数,相当于给它的参数fn做了一个赋值操作(RHS查询),foo函数调用位置是在doFoo内
所以它会先找这里面有没有a,没有继续往上找也就是全局作用域了,然后它找到了a,最后得到了我们的输出结果。
如果我们把上面a=4的放开就会发现this.a的结果此时变为了4。
需要注意的是这段代码需要在浏览器环境下执行,node会出现undefined,去掉声明var就行了。
此外我们还可以得知foo就是一个回调函数,这里回调函数丢失了this的绑定,我们实际上要使用的是
obj里面的a,但是结果显示并非如此,那么我们如何通过this获取obj里面a的值呢,这就是下一部分要说的了。
显式绑定
在解决前面问题之前呢,这里引入显示绑定的概念。
在JavaScript中我们可以通过call和apply这两个方法,初步将this绑定到指定的对象上去。
因为我们可以直接指定this绑定的对象,所以叫它显式绑定。
下面就说下这两个函数是如何工作的
call和apply的使用
它的语法是这样的
func.call([thisArg[, arg1, arg2, ...argN]])
它的第一参数代表是一个对象,这个对象会绑定到this上,接着在调用函数时指定这个this
第二个参数开始后面是都是要传入的参数,通过逗号间隔开。举个简单的例子
function foo(x,y,z){
console.log(this.a,x,y,z)
}
var obj = {
a:2
}
foo.call(obj,1,2,3) //输出2 1 2 3
通过foo.call,在调用foo时我们强制把this绑定到了obj上去。
apply的使用方法和call是一样的,区别就在于参数部分。
apply的语法是
func.apply(thisArg, [ argsArray])
它和call唯一的区别就是参数上的区别,第一个参数是this要绑定的对象,第二个参数成了一个数组的形式。
这个数组在传入函数的时候,实际上会进行类型python语言的拆包操作(不知道这么说对不对,意思大概是对的)。
一个简单的例子
function foo(something) {
console.log(this.a, something);
return this.a + something;
}
var obj = {
a: 2
};
var bar = function () {
return foo.apply(obj, arguments); //this绑定到obj对象
};
var b = bar(3); // 输出:2 3
因为我们使用apply进行显式绑定,所以后面无论怎么调用bar
它都会去将this绑定到obj之后调用foo函数。这种绑定方式是一种显式的强制性的,所以我们称之为硬绑定。
因为硬绑定这种模式经常使用,所以ES5提供了内置方法Function.prototype.bind,来简化这个操作。
在使用之前先了解下它的语法
let boundFunc = func.bind(thisArg[, arg1[, arg2[, ...argN]]])
bind()方法主要就是将函数绑定到某个对象,bind()会创建一个函数,函数体内的this对象的值会被绑定到传入bind()中的第一个参数的值,例如:func.bind(obj),实际上可以理解为obj.func(),这时func函数体内的this自然指向的是obj;
我们可以通过下面的代码实现bind的功能
if (!Function.prototype.bind) {
Function.prototype.bind = function () {
var self = this, // 保存原函数
context = [].shift.call(arguments), // 保存需要绑定的this上下文
args = [].slice.call(arguments); // 剩余的参数转为数组
return function () { // 返回一个新函数
// 执行新函数时,将传入的上下文context作为新函数的this
// 并且组合两次分别传入的参数,作为新函数的参数
self.apply(context,[].concat.call(args, [].slice.call(arguments)));
}
}
}
本质上bind就是apply的一个封装,方便我们使用罢了。
通过使用bind我们就解决了上面提到的回调函数丢失了this绑定的问题。
JavaScript内置函数实现this的绑定
第三方库的许多函数,以及 JavaScript 语言和宿主环境中许多新的内置函数,都提供了一 个可选的参数,通常被称为“上下文”(context),其作用和 bind(..) 一样,确保你的回调 函数使用指定的 this。
举例来说:
function foo(el) {
console.log( el, this.id );
}
var obj = {
id: "awesome"
};
// 调用 foo(..) 时把 this 绑定到 obj [1, 2, 3].forEach( foo, obj );
// 1 awesome 2 awesome 3 awesome
这些函数实际上就是通过 call(..) 或者 apply(..) 实现了显式绑定,这样你可以少些一些 代码
new绑定
首先要说明的是JavaScript中new的机制实际上和面向类的语言完全不同。
JavaScript中的"构造函数"并不属于某个类,也不会实例化一个类,它只不过是一个能被new调
用的普通函数而已。
看一段代码
function foo(a) { this.a = a;
}
var bar = new foo(2); console.log( bar.a ); // 2
使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。
- 创建(或者说构造)一个全新的对象。
- 这个新对象会被执行[[原型]]连接。
- 这个新对象会绑定到函数调用的this。
- 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
使用 new 来调用 foo(..) 时,我们会构造一个新对象并把它绑定到 foo(..) 调用中的 this 上。new 是最后一种可以影响函数调用时 this 绑定行为的方法,我们称之为 new 绑定。
这几种this绑定的优先级
我们先介绍前四种this绑定规则,那么问题来了,如果一个函数调用存在多种绑定方法,this最终指向谁呢?这里我们直接先上答案,this绑定优先级为:
显式绑定 > 隐式绑定 > 默认绑定
new绑定 > 隐式绑定 > 默认绑定
为什么显式绑定不和new绑定比较呢?因为不存在这种绑定同时生效的情景,如果同时写这两种代码会直接抛错,所以大家只用记住上面的规律即可。
function foo(){
this.a = 3;
};
var obj = {
a:2
}
var f=new foo().call(obj);//报错 call is not a function
箭头函数的this
后续。。
参考资料
YouDontKnowJS
https://github.com/lin-xin/blog/issues/7