一、使用this的原因(why)
对于前端开发者来说,this关键字是JavaScript中最复杂的机制之一,不同的位置使用,指向是不一样的,所以它到底有什么作用呢?为什么要花大量的时间去研究他呢
- this提供了一种优雅的方式来隐式的“传递”一个对象的引用,可以方便我们简洁的设计API。特别是当结构越来越复杂时。
function GetObj (name) {
this.name = name
}
GetObj.prototype.showName = function (){
alert(this.name) // this指代GetObj这个对象
}
let p1 = new GetObj('张三')
p1.showName()
二、了解this(what----this到底是什么)
1、对this的误解
- 认为this指向函数自身
function foo(num) {
console.log('foo===', num) // foo: 6 // foo: 7 // foo: 8 // foo: 9
// 记录 foo 被调用的次数
this.count++
}
foo.count = 0
for(var i = 0; i< 10; i++){
if (i > 5) {
foo( i );
}
}
console.log( foo.count ); // 0
this并没有指向自身的foo函数,而是指向了window。
- 认为this的作用域指向函数的作用域
在某种情况下它 是正确的,但是在其他情况下它却是错误的
function foo(){
var a = 2;
this.bar();
}
function bar() {
console.log(this); //window
console.log(this.a); // undefined
}
foo();
这个a作用域存在于foo函数中,所以在window这个作用域中找不到。但是由于var定义的a,在词法分析阶段会将a进行变量提升,所以window中会有一个a,但是没有值
2、与自然语言的对比
实际上js中的this和我们自然语言中的代词有类似性。比如英语中我们写"小明,你吃了么?"
注意上面的代词"小明",我们当然可以这样写:"小明,小明吃了么?" ,这种情况下我们没有使用this去重用代替小明。
主要和执行时候的上下文环境有关联
3、那this到底是什么呢?
this只跟函数的调用位置有关,是在函数被调用时发生绑定的;this的指向取决于函数在哪里被调用。
this指向的最终对象,跟调用位置以及应用的绑定规则有关
三、绑定规则(how--如何寻找函数的调用位置)
1、 默认绑定
- 最常见的绑定规则,独立函数调用时默认绑定,也可以看做无法应用其他绑定规则时的默认规则
function foo() {
console.log( this.a ); // this指向window
}
var a = 2;
foo(); // 2
- this指向
- 在全局环境中,this 指向全局对象,在浏览器中,它就是 window 对象。
- 普通函数是在全局环境中被调用:
非严格模式下:指向全局对象window
严格模式下:指向undefined(使用严格模式的时候,全局对象无法使用默认绑定)
function foo() {
"use strict";
console.log( this ); // undefined
console.log( this.a ); // Uncaught TypeError: Cannot read property 'a' of undefined
}
"use strict";
console.log( this ) // window
var a = 2;
foo();
2、 隐式绑定
- 当函数的调用存在上下文对象,或者说
是否被某个对象拥有或者包含
。this将会被绑定到这个上下文对象。
this 始终会指向直接调用函数的上一级对象
function foo() {
console.log( this.a ); // 指向obj
}
var obj = {
a: 2,
foo: foo //--->foo: function () { console.log( this.a ); }
};
obj.foo(); // 2
- 函数嵌套,每个function函数(非箭头函数)在每次调用时都会在函数内生成一个自己的this。
当两个函数嵌套定义时,内层函数中的this与外层函数中的this是完全独立的。函数内this的值是在函数调用时才确定的,函数的调用方式不同,this也就不同
1,当函数直接调用时 fn(); 函数内this的值是window对象,(在js严格模式"use strict";下,函数内this的值是null)
2,当把这个函数赋值给一个对象的方法obj.abc = fn;
调用obj.abc()时,函数内this的值是obj对象,也就是函数所在的对象。
var obj = {
y: function() {
console.log(this === obj); // true
console.log(this); // Object {y: function}
fn(); // 嵌套的函数不是对象的方法,直接调用,所以this指向window
function fn() {
console.log(this === obj); // false
console.log(this); // Window 全局对象
}
}
}
obj.y();
- 问题:被隐式绑定的函数会丢失绑定对象,会应用默认绑定,从而把 this 绑定到全局对象或者 undefined 上,取决于是否是严格模式。
1、 函数引用传递给新的全局变量
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // bar是全局变量
var a = "oops, global"; // a 是全局对象的属性
bar(); // "oops, global",bar() 其实是一个不带任何修饰的函数调用
2、将函数作为参数传入回调函数中
function foo() {
console.log( this.a );
}
function doFoo(fn) { // fn 其实引用的是 foo
fn(); // <-- 调用位置!
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // a 是全局对象的属性
doFoo( obj.foo ); // "oops, global"
3、显式绑定
使用 call()和apply() 方法进行强制绑定
- 他们的第一个参数都是指定函数运行时的this指向,第一个参数不传(参数为空)。或者为null、undefined。默认 this 指向全局对象(非严格模式)或 undefined(严格模式)。
var x = 1;
var obj = {
x: 2
}
function fn() {
console.log(this);
console.log(this.x);
}
fn.call(obj)
// Object {x: 2}
// 2
fn.apply(obj)
// Object {x: 2}
// 2
fn.call()
// Window 全局对象
// 1
fn.apply(null)
// Window 全局对象
// 1
fn.call(undefined)
// Window 全局对象
// 1
- 使用 call() 和 apply() 传入的this对象为原始值(字符串类型,布尔类型或者数字类型),这个原始值就会被转换成它的对象形式(new String()、new Bollean()、new Number())
- 区别
call()的第二个以及后续的参数是一个列表
apply()的第二个参数是一个数组,所有的参数放在这个数组中
参数列表和参数数组都将作为函数的参数进行执行
var x = 1
var obj = {
x: 2
}
function sum (y, z) {
console.log(this.x + y +z)
}
sum(3, 4) // 8
sum.call(obj, 3, 4) // 9
sum.apply(obj, [3, 4]) // 9
使用 bind() 方法进行强制绑定
调用 f.bind(someObject) 会创建一个与 f 具有相同函数体和作用域的函数,但是在这个新函数中,新函数的 this 会永久的指向 bind 传入的第一个参数
,无论这个函数是如何被调用的。
var x = 1
var obj1 = {
x: 2
}
var obj2 = {
x: 3
}
function fn () {
console.log(this)
console.log(this.x)
}
var a = fn.bind(obj1)
var b = a.bind(obj2)
fn() // window 1
a() // {x: 2} 2
b() // {x: 2} 2
a.call(obj2) // {x: 2} 2
在上面的例子中,虽然我们尝试给函数 a 重新指定 this 的指向,但是它依旧指向第一次 bind 传入的对象,即使是使用 call 或 apply 方法也不能改变这一事实,即永久的指向 bind 传入的第一次参数。
4、new绑定
使用 new 关键字,通过构造函数生成一个实例对象。此时,this 便指向这个新对象
var x = 1;
function Fn() {
this.x = 2;
console.log(this); // Fn {x: 2}
}
var obj = new Fn(); // obj和Fn(..)调用中的this进行绑定
console.log(obj.x) // 2
绑定规则优先级
如果某个调用位置可以应用多条规则该怎么办?为了 解决这个问题就必须给这些规则设定优先级。
毫无疑问,默认绑定的优先级别最低。
隐式绑定和显示绑定谁的优先级别更高呢?
function fn(){
console.log(this.a)
}
let obj1 = {
a: 1,
fn: fn
}
let obj2 = {
a: 2,
fn: fn
}
obj1.fn()//1
obj2.fn()//2
obj1.fn.call(obj2)//2
obj2.fn.apply(obj1)//1
- 可以看到,显式绑定优先级更高
隐式绑定和new()绑定谁的优先级别更高呢?
function foo(something) {
this.a = something;
}
var obj1 = {
foo: foo
};
var obj2 = {};
obj1.foo( 2 );
console.log( obj1.a ); // 2
obj1.foo.call( obj2, 3 );
console.log( obj2.a ); // 3
var bar = new obj1.foo( 4 );
console.log( obj1.a ); // 2
console.log( bar.a ); // 4
- 可以看到 new 绑定比隐式绑定优先级高。
但是 new 绑定和显式绑定谁的优先级更高呢
function foo(something) {
this.a = something;
}
var obj1 = {};
var bar = foo.bind( obj1 );
bar( 2 );
console.log( obj1.a ); // 2
var baz = new bar(3);
console.log( obj1.a ); // 2
console.log( baz.a ); // 3
bar 被硬绑定到 obj1 上,但是 new bar(3) 并没有像我们预计的那样把 obj1.a 修改为 3。相反,new 修改了硬绑定(到 obj1 的)调用 bar(..) 中的 this。因为使用了 new 绑定,我们得到了一个名字为 baz 的新对象,并且 baz.a 的值是 3。
当某个函数调用应用了这四种规则中的多条,那么优先级:new绑定 > 显示绑定 > 隐式绑定 > 默认绑定
5、箭头函数中this指向
箭头函数表达式的语法比函数表达式更简洁,并且没有自己的this,arguments,super或new.target
。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。
箭头函数没有自己的this绑定。箭头函数中使用的this,其实是直接包含它的那个函数或函数表达式中的this
var a = 1;
let obj = {
a: 2,
fn1: () => {
console.log('fn1',this.a) // 1 this指向window
let fn3 = () => {
console.log('fn3',this.a) // 1 this指向window
}
fn3()
let fn4 = () => {
console.log('fn4',this.a) // 1 this指向window
}
fn4.call(obj)
// 由于箭头函数没有自己的this指针,通过 call() 或apply() 方法调用一个函数时,只能传递参数,他们的第一个参数会被忽略。
fn4.apply(obj)
},
fn2: function() {
console.log('fn2',this.a) // 2 this指向obj
}
}
obj.fn1()
obj.fn2()
箭头函数的this看外层的是否有函数,如果有,外层函数的this就是内部箭头函数的this,如果没有,则this是window。- 从父作用域继承this
同 bind 一样,箭头函数也很“顽固”,无法通过 call 和 apply 来改变 this 的指向,即传入的第一个参数被忽略。
图片转自掘金小册-前端面试之道
总结
如果要判断一个运行中函数的 this 绑定,就需要找到这个函数的直接调用位置。找到之后 就可以顺序应用下面这四条规则来判断 this 的绑定对象。
- 由 new 调用?绑定到新创建的对象。
- 由 call 或者 apply(或者 bind)调用?绑定到指定的对象。
- 由上下文对象调用?绑定到那个上下文对象。
- 默认:在严格模式下绑定到 undefined,否则绑定到全局对象。
一定要注意,有些调用可能在无意中使用默认绑定规则。如果想“更安全”地忽略 this 绑 定,你可以使用一个 DMZ 对象,比如 ø = Object.create(null),以保护全局对象。
ES6 中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定 this,具体来说,箭头函数会继承外层函数调用的 this 绑定(无论 this 绑定到什么)。这 其实和 ES6 之前代码中的 self = this 机制一样。
https://www.cnblogs.com/kidsitcn/p/10985338.html
https://zhuanlan.zhihu.com/p/71490991
https://zhuanlan.zhihu.com/p/28536635
嵌套函数指向