一、this到底是什么?
this是一个特别的关键字,被自动定义在所有函数的作用域中。
this是在运行时进行绑定的,并不是在编写时绑定,他的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
当一个函数被调用时,会创建一个活动记录(执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息。this就是这个记录的一个属性,会在函数执行的过程中用到。
this实际上是在函数被调用时发生的绑定,他指向什么完全取决于函数在哪里被调用。
二、两个误解
1.this指向函数自身
2.this指向它的作用域
三、this绑定规则
看看在函数的执行过程中调用位置如何决定this的绑定对象
1.默认绑定(可以把这条规则看作是无法应用其他规则时的默认规则)
function foo(){
console.log(this.a);
}
let a =2;
foo(); //2
函数调用时应用了this的默认绑定,因此this指向全局对象。在代码中,foo()是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定。
注:如果使用严格模式(strict mode),则不能将全局对象用于默认绑定,因此this会绑定到undefined:
function foo(){
“use strict”
console.log(this.a);
}
let a =2;
foo(); //TypeError:this is undefined
虽然this的绑定规则完全取决于调用位置,但是只要foo()运行在非strict mode时,默认绑定才能绑定到全局对象;在严格模式下调用foo()则不影响默认绑定:
function foo(){
“use strict”
console.log(this.a);
}
let a =2;
(function(){
foo(); //2
})();
2.隐式绑定
需要考虑的规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含。
function foo(){
console.log(this.a);
}
const obj = {
a =2,
foo:foo
};
obj.foo(); //2
调用位置会使用obj上下文来引用函数,因此你可以说函数被调用时obj对象“拥有”或者“包含”函数引用。
当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。
注:隐式丢失
一个最常见的this绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说他会应用默认绑定,从而把this绑定到全局对象或者undefined上,取决于是否是严格模式。
我们会通过下面的固定this方法来修复这个问题
3.显式绑定
在分析隐式绑定时,我们必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把this间接(隐式)绑定到这个对象上。
那么如果我们不想在对象内部包含函数引用,而想在某个对象上强制调用函数,可以使用下面方法:
1.call() ;apply()
他们的第一个参数是一个对象,是给this准备的,接着再调用函数时将其绑定到this。因为你可以直接指定this的绑定对象,因此我们称之为显式绑定。
如果你传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作this的绑定对象,这个原始值会被转换成它的对象形式(也就是new String(..)、new Boolean(..)、new Number(..)).这通常被称为“装箱”。
2.显式绑定仍然无法解决我们之前提出的丢失绑定问题,但是显式绑定的一个变种可以解决这个问题
(1)硬绑定: 通过apply + 闭包机制 实现bind方法,实现强行绑定规则
function foo(b){
return this.a+b
}
var obj={
a:2
}
function bind(fn,obj){
return function(){
return fn.apply(obj,arguments)
}
}
bind(foo,obj)(3)//5
硬绑定是一种非常常用的模式,所以ES5提供了内置的方法Function.porpertype.bind,用法如下:
function foo(b){
return this.a+b
}
var obj={
a:2
}
var bar = foo.bind(obj);
bar b = bar(3)
console.log(b);//5
bind(..)会返回一个硬编码的新函数,他会把你指定的参数设置为this的上下文并调用原始函数。
(2)API调用的“上下文”
第三方库的许多函数,以及JS语言和宿主环境中许多新的内置函数,都提供了一个可选的参数,通常被称为“上下文”(context),其作用和bind(..)一样,确保你的回调函数使用指定的this
function foo (el){
console.log(el,this.id)
}
var obj ={
id:'some one'
};
[1,2,4].forEach(foo,obj)
// 1 some one 2 some one 4 some one
4.new绑定
- 传统面向类的语言中的构造函数,是在使用new操作符实例化类的时候,会调用类中的一些特殊方法(构造函数)
- 很多人认为js中的new操作符和传统面向类语言的构造函数是一样的,其实有很大的差别
- 从新认识一下js中的构造函数,js中的构造函数 在被new操作符调用时,这个构造函数不属于每个类,也不会创造一个类,它就是一个函数,只是被new操作符调用。
- 使用new操作符调用 构造函数时会执行4步
- 创建一个全新的对象
- 对全新的对象的__proto__属性地址进行修改成构造函数的原型(prototype)的引用地址
- 构造函数的this被绑定为这个全新的对象
- 如果构造函数有返回值并且这个返回值是一个对象,则返回该对象,否则返回当前新对象
function Foo(a){
this.a=a
}
var F = new Foo(2)
console.log(F.a)//2
注:在new中使用硬绑定函数,主要的目的是预先设置函数的一些参数,这样在使用new进行初始化时就可以只传入其余的参数。bind(...)的功能之一就是可以把除了第一个参数(第一个参数用于绑定this)之外的其他参数都传给下层的函数(这种技术称为“部分应用”,是“柯里化”的一种)
举例来说
function foo(a,b){
this.val = a+b;
}
//之所以使用null是因为本例中我们不关心硬绑定的this是什么
反正使用new时this会被改变
var bar = foo.bind(null,"a");
bar baz = new bar("b")
baz.val;//ab
四、this判断
- 1 判断该函数是不是被new操作符调用,有的话 this就是 构造函数运行时创建的新对象 var f = new foo()
- 2 判断 函数是不是使用显式绑定 call、apply、bind,如果有,那么该函数的this就是 这个三个方法的第一个参数 foo.call(window)
- 3 判断该函数是不是被一个对象的属性引用了地址,该函数有上下文(隐式绑定),在函数执行的时候是通过该对象属性的引用触发,这个函数的this就是当前对象的。 obj.foo();
- 4 上面的三种都没有的话,就是默认绑定,该函数的this就是全局对象或undefined(严格模式下)
五、绑定例外
1.被忽略的this
把 null、undefined通过 apply、call、bind 显式绑定,这些值在调用时会被忽略,应用的是默认绑定。但是建议这么做因为在非严格的模式下会给全局对象添加属性,有时候会造成不可必要的bug。
那么什么时候我们会传入null?
(1)使用apply(..)来“展开”一个数组,并当作参数传入一个函数
注:在ES6中可以使用...操作符来代替apply(..)来“展开”数组
(2)bind(..)对参数进行柯里化(预先设置一些参数)
但是总是使用null来忽略this绑定可能会产生一些副作用。如果摸个函数确实是用了this,那默认绑定规则会把this绑定到全局对象,这价格、能够导致不可预计的后果。
更安全的this
一种更安全的做法是传入一个特殊的对象,把this绑定到这个对象上。
如果我们在忽略this绑定时总是传入一个DMZ对象,那就不用担心了,因为任何对于this的使用都会被限制在这个空对象中,不会对全局对象产生任何影响
在JS中创建一个空对象最简单的方法是Ob'j'ect.create(null)。
Ob'j'ect.create(null)和{}很像,淡但是并不会创建Ob'j'ect.prototype这个委托,所以他比{}”更空“
function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// 我们的空对象
var ø = Object.create( null );
// 把数组展开成参数
foo.apply( ø, [2, 3] ); // a:2, b:3
// 使用 bind(..) 进行柯里化
var bar = foo.bind( ø, 2 );
bar( 3 ); // a:2, b:3