阅读 434

JavaScript之你不知道的this

很重要的一句话

只有深谙了this,你才有可能用 JavaScript 创建类似谷歌地图这样大型的复杂应用


一、这篇文章出现的背景

1. this在我们开发过程中的重要性(开发场景) -- 通过一段代码简单了解this

提供了一种更优雅的方式来隐式”传递”一个对象引用, 让API设计更加简洁和清晰

首先来看一段代码, 此处不使用this, 需要给identify()和speak()显示的传入一个上下文对象:

// 定义 you & me对象
var me = {
    name: "Kyle"
};
var you = {
    name: "Reader"
};

function identify(context) {
    return context.name.toUpperCase();
}
function speak(context) {
    var greeting = "Hello, I'm " + identify( context );
    console.log( greeting );
}

identify( you ); // READER
speak( me ); //hello, 我是 KYLE
复制代码

使用this解决: 可以在不同的上下文对象(me 和 you)中重复使用函数identify()和speak()

function identify() {
    return this.name.toUpperCase();
}
function speak() {
    var greeting = "Hello, I'm " + identify.call( this );
    console.log( greeting );
}

identify.call( me ); // KYLE
identify.call( you ); // READER

speak.call( me ); // Hello, 我是 KYLE
speak.call( you ); // Hello, 我是 READER
复制代码

显然, 随着你的使用模式越来越复杂, 显式传递上下文对象会让代码变得越来越混乱, this可以让你的代码变得更优雅。特别是当你使用对象(关联)和原型时, 利用this使得函数可以自动引用合适的上下文对象显的尤为重要

2.两种错误的理解

  • this指向函数自身
  • this指向函数的作用域, 这个在某些情况下是正确的, 但是在其他情况下确实错误的

事实上, 一部分人认为"this既不指向函数自身也不指向函数的词法作用域", 但是也是不对的, 在某种情况下, this就指向函数自身, 也可能指向词法作用域

3.本质

this是在运行(函数被调用)时发生绑定的,并不是在编写时绑定, 它的上下文取决于函数调用时的各种条件,它指向什么完全取决于函数在哪里被调用


二、this 绑定规则 & 优先级

简单来说, 有这大致四种

  1. 由new调用(new绑定)
  2. 函数是否通过call、apply(显式绑定)或者硬绑定调用
  3. 函数是否在某个上下文对象中调用(隐式绑定)
  4. 默认绑定

1. 默认绑定

无法应用其他规则时的默认规则, 严格模式下绑定到undefined, 否则绑定到全局对象

最常用的函数调用类型:独立函数调用

function foo() {
    console.log( this.a );
}

var a = 2;
foo(); // 2
复制代码

代码中, foo()是直接使用不带任何修饰的函数引用进行调用的,只能适用于this的默认绑定,无法应用其他规则,因此this指向全局对象

// 严格模式下
function foo() {
    "use strict";
    console.log( this.a );
}

var a = 2;
foo(); // TypeError: this is undefined
复制代码

所以, 不推荐这种写法。

2. 隐式绑定

考虑调用位置是否有上下文对象,或者说是否被某个对象或者包含

当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象

必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把 this 间接(隐式)绑定到这个对象上

function foo() {
    console.log(this.a);
}
var obj = {
    a: 2,
    foo: foo
};

obj.foo(); // 2
复制代码

对象属性引用链中只有最后一层会影响调用位置, 即调用栈的末端

function foo() {
    console.log( this.a );
}
var obj2 = {
    a: 42,
    foo: foo
};
var obj1 = {
    a: 2,
    obj2: obj2
};

obj1.obj2.foo(); // 42
复制代码

3. 显示绑定

强制指定某些对象对函数进行调用,this则强制指向调用函数的对象

  • call(thisObj, arg1, arg2, arg3...)
  • apply(thisObj, argArr)
  • ES5 中提供了内置的方法 硬绑定bind(thisObj)

显示绑定场景

function foo() {
    console.log( this.a );
}
var obj = {
    a:2
};

foo.call( obj ); // 2
复制代码

硬绑定常用场景

function foo(something) {
    console.log( this.a, something );
    return this.a + something;
}
var obj = {
    a:2
};

var bar = foo.bind( obj );
var b = bar( 3 ); // 2 3
console.log( b ); // 5
复制代码

4. new绑定

new方式优先级最高,只要是使用new方式来调用一个构造函数,this一定会指向new调用函数新创建的对象

function foo(a) {
    this.a = a;
}
var bar = new foo(2);

console.log( bar.a ); // 2
复制代码

三、绑定例外

1. 箭头函数

,实际原因是箭头函数根本没有自己的this

this指向的固定化,并不是因为箭头函数内部有绑定this的机制, 实际原因箭头函数没有自己的this,它的this是继承而来,默认指向在定义它时所处的对象(宿主对象)。 捕获其所在(即定义的位置)上下文的this值,作为自己的this值, 如果在当前的箭头函数作用域中找不到变量,就像上一级作用域里去找, 导致内部的this就是外层代码块的this

// demo 1
function foo() {
	 setTimeout(() => {
	    console.log('id:', this.id);
	  }, 100);
}
var id = 21;
foo.call({ id: 42 }) // id: 42

//demo 2
function Person() {
    this.name = 'dog';
    this.age = '18';
    setTimeout( () => {
        console.log(this);
        console.log('my name:' + this.name + '& my age:' + this.age)
    }, 1000)
}
var p = Person();

复制代码

2. 被忽略的this

当被绑定的是null,则使用的是默认绑定规则

// 如果你把 null 或者 undefined 作为 this 的绑定对象传入 call 、 apply 或者 bind ,这些值在调用时会被忽略,
实际应用的是默认绑定规则
function foo() {
	console.log( this.a );
}
var a = 2222;
foo.call( null ); // 2222
复制代码

四、(隐式)绑定丢失

最常见的this绑定问题就是被隐式绑定的函数会丢失绑定对象,也是就说它会应用默认绑定,从而把this绑定到全局对象或者undefined上,取决于是否是严格模式

1. 引用赋值丢失

function foo() {
    console.log( this.a );
}
var obj = {
    a: 2,
    foo: foo
};
var bar = obj.foo; // 函数别名!
var a = "oops, global"; // a是全局对象的属性

bar(); // "oops, global"
复制代码

虽然 bar 是 obj.foo 的一个引用,但是实际上,它引用的是 foo 函数本身,这就相当于:var bar = foo, obj对象只是一个中间桥梁, obj.foo只起到传递函数的作用,所以bar跟obj对象没有任何关系,此时的 bar() 其实是一个不带任何修饰的函数调用. 而bar本身又不带a属性,因此应用了默认绑定,最后a只能指向window.

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. 回调函数丢失

function thisTo(){
   console.log(this.a);
}
var data={
    a:2,
    foo:thisTo //通过属性引用this所在函数
};
var a=3;//全局属性

setTimeout(data.foo,100);// 3
复制代码

所谓传参丢失,就是在将包含this的函数作为参数在函数中传递时,this指向改变

setTimeout函数的本来写法应该是setTimeout(function(){......},100); 100ms后执行的函数都在“......”中, 可以将要执行函数定义成var fun = function(){......}, 即:setTimeout(fun,100),100ms后就有:fun();所以此时此刻是data.foo作为一个参数,是这样的:setTimeout(thisTo,100);100ms过后执行thisTo(), 实际道理还跟1.1差不多,没有调用thisTo的对象,this只能指向window 实际上你没办法控制回调函数的执行方式,没有办法控制会影响绑定的调用位置. 因此, 回调函数丢失this绑定是非常常见的,甚至更加出乎意料的是,调用回调函数的函数可能会修改this,特别 是在一些流行的JavaScript库中时间处理器会把回调函数的this强制绑定到触发事件的DOM元素上


总结

四种规则:

  • 由new调用(new绑定)
  • 通过call、apply(显式绑定)或者硬绑定调用
  • 函数是否在某个上下文对象中调用(隐式绑定)
  • 默认绑定

特殊情况特殊处理:

  • 箭头函数
  • 被忽略的this
  • 绑定丢失

最后, 希望大家早日实现:成为前端高手的伟大梦想!!!
欢迎交流~

作者个人微信:wanderlian

作者个人微信公众号: 连先生有猫病

关注下面的标签,发现更多相似文章
评论