JavaScript夯实基础系列(三):this

  在JavaScript中,函数的每次调用都会拥有一个执行上下文,经过this关键字指向该上下文。函数中的代码在函数定义时不会执行,只有在函数被调用时才执行。函数调用的方式有四种:做为函数调用做为方法调用做为构造函数调用以及间接调用,断定this指向的规则跟函数调用的方式有关。
html

1、做为函数的调用

  做为函数调用是指函数独立执行,函数没有人为指定的执行上下文。在有些状况下,做为函数调用的形式具备迷惑性,不只仅是简单的函数名后面加括号来执行。
数组

一、明确的做为函数调用

  明确的做为函数调用是指形如func(para)形式的函数调用。做为函数调用的状况下this在严格模式下为undefined,在非严格模式下指向全局对象(在浏览器环境下为Window对象)以下代码所示:
浏览器

var a = 1;
function test1 () {
    var a = 2
    return this.a
}
test1() // 1
复制代码
'use strict'
var a = 1;
function test1 () {
    var a = 2
    return this.a
}
test1() // Uncaught TypeError
复制代码

  以函数调用形式的函数一般不使用this,可是能够根据this来判断当前是不是严格模式。以下代码所示,在严格模式下,this为undefined,strict为true;在非严格模式下,this为全局对象,strict为false。
app

var strict = (function () {
    return !this
})()
复制代码

二、对象做为桥梁找到方法

  经过对象调用的函数称为方法,可是经过对象找到方法并不执行属于做为函数调用的状况。以下代码所示:
ide

var a = 1;

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

var obj = {
    a: 2,
    test: test
};

var func = obj.test;

func(); // 1
复制代码

  上述代码中,obj.test是经过obj对象找到函数test,并未执行,找到函数以后将变量func指向该函数。obj对象在这个过程当中只是起到一个找到test地址的桥梁做用,并不固定为函数test的执行上下文。所以var func = obj.test;执行的结果仅仅是变量func和变量test指向共同的函数体而已,所以func()仍然是做为函数调用,和直接调用test同样。
  当传递回调函数时,本质也是做为函数调用。以下代码所示:
函数

var a = 1

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

function test(fn) {
    fn();
}

var obj = {
    a: 2,
    func: func
};

test( obj.func ); // 1
复制代码

  函数参数是以值传递的形式进行的,obj.func做为参数传递进test函数时会被复制,复制的仅仅是指向函数func的地址,obj在这个过程当中起到找到函数func的桥梁做用,所以test函数执行时,里面的fn是做为函数调用的。
  接收回调的函数是本身写的仍是语言内建的没有什么区别,好比:
ui

var a = 1;

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

var obj = {
    a: 2,
    test: test
};

setTimeout( obj.test, 1000 ); // 1
复制代码

  setTimeout的第一个参数是经过obj对象找到的函数test,本质上obj依然是起到找到test函数的桥梁做用,所以test依然是做为函数调用的。
this

三、间接调用传递null或undefined做为执行上下文

  函数的间接调用是指经过call、apply或bind函数明确指定函数的执行上下文,当咱们指定null或者undefined做为间接调用的上下文时,函数实际是做为函数调用的。可是有一点须要注意:call()和apply()在严格模式下传入空值则上下文为空值,并非由于遵循做为函数调用在严格模式下执行上下文为全局对象的规则,而是由于在严格模式下call()和apply()的第一个实参都会变成this的值,哪怕传入的实参是原始值甚至是null或undefined。
spa

var a = 1;

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

test.call( null ); // 1
复制代码

  间接调用的目的是为了指定函数的执行上下文,那么为何要传null或undefined使其做为函数调用呢?这是由于咱们会用到这些方法的其余性质:函数call中通常不传入空值(null或undefined);函数apply传入空值能够起到将数组散开做为函数参数的效果;函数bind能够用来进行函数柯里化。在ES6中,新增了扩展运算符‘...’,将一个数组转为用逗号分隔的参数序列,能够替代往apply函数传空值的状况。可是ES6中没有增长函数柯里化的方法,所以往函数bind中传空值的状况将继续使用。
  在使用apply或bind传入空值的状况,通常是不关心this值。可是若是函数中使用了this,在非严格模式下可以访问到全局变量,有时会违背代码编写的本意。所以,使用一个真正空的值传入其中可以避免这类状况,以下代码所示:
prototype

var empty = Object.create( null );
      
function foo(a,b) {
    console.log( "a:" + a + ", b:" + b );
}
      
foo.apply( empty, [1, 2] ); // a:1, b:2
复制代码

2、做为方法调用

  当函数挂载到一个对象上,做为对象的属性,则称该函数为对象的方法。若是经过对象来调用函数时,该对象就是本次调用的上下文,被调用函数的this也就是该对象。以下代码所示:

var obj = {
    a: 1,
    test: test
};

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

obj.test(); // 1
复制代码

  在JavaScript中,对象能够拥有对象属性,对象属性有又能够拥有对象或者方法。函数做为方法调用时,this指向直接调用该方法的对象,其余对象仅仅是为了找到this指向的这个对象而已。以下代码所示:

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

var obj2 = {
    a: 2,
    test: test
};

var obj1 = {
    a: 1,
    obj2: obj2
};

obj1.obj2.test(); // 2
复制代码

  当方法的返回值时一个对象时,这个对象还能够再调用它的方法。当方法不须要返回值时,最好直接返回this,若是一个对象中的全部方法都返回this,就能够采用链式调用对象中的方法。以下代码所示:

function add () {
    this.a++;
    return this;
}

function minus () {
    this.a--;
    return this;
}

function print() {
    console.log( this.a );
    return this;
}

var obj = {
    a: 1,
    print: print,
    minus: minus,
    add: add
};

obj.add().minus().add().print(); // 2
复制代码

3、做为构造函数调用

  在JavaScript中,构造函数没有任何特殊的地方,任何函数只要是被new关键字调用该函数就是构造函数,任何不被new关键字调用的都不是构造函数。
  当使用new关键字来调用函数时,会经历如下四步:

一、建立一个新的空对象。
二、这个空对象继承构造函数的prototype属性。
三、构造函数将新建立的对象做为执行上下文来进行初始化。
四、若是构造函数有返回值而且是对象,则返回构造函数的返回值,不然返回新建立的对象。

  约定俗成的是:在编写构造函数时函数名首字母大写,且构造函数不写返回值。所以通常来讲,new关键字调用构造函数建立的新对象做为构造函数的this。以下代码所示:

function foo() {
    this.a = 1;
}

var bar = new foo();
console.log( bar.a ); // 1
复制代码

4、间接调用

  在JavaScript中,对象中的方法属性仅仅存储的是一个函数的地址,函数与对象的耦合度没有想象中的高。经过对象来调用函数,函数的执行上下文(this指向)就是该对象。若是经过对象来找到函数的地址,就能指定函数的执行上下文,可使用call()、apply()和bind()方法来实现。换而言之,任何函数能够做为任何对象的方法来调用,哪怕函数并非那个对象的方法。

一、call()和apply()

  每一个函数都call()和apply()方法,函数调用这两个方法是能够明确指定执行上下文。从绑定上下文的角度来讲这两个方法是同样的,第一个参数传递的都是指定的执行上下文。所不一样的在于call()方法剩余的参数将会做为函数的实参来使用,能够有多个;apply()则最多只接收两个参数,第一个是执行上下文,第二个是一个数组,数组中的每一个元素都将做为函数的实参。以下代码所示:

var a = 1
function test(b,c) {
    console.log(`a:${this.a},b:${b},c:${c}`)
}
var obj = {
    a:2
}
test.call(obj,3,4) // a:2,b:3,c:4

var d = 11
function test2(b,c) {
    console.log(`b:${b},c:${c},d:${this.d}`)
}
var obj2 = {
    d:12
}
test2.apply(obj2,[13,14]) // b:13,c:14,d:12
复制代码

  在非严格模式下,call()、apply()的第一个参数传入null或者undefined时,函数的执行上下文被替代为全局对象,若是传入的是基础类型,则为替代为相应的包装对象。在严格模式下,遵循的规则是传入的值即为执行上下文,不替换,不自动装箱。以下代码所示:

var a = 1
function test1 () {
    console.log(this.a)
}
test1.call(null) // 1
test1.call(undefined) // 1
test1.apply(null) // 1
test1.apply(undefined) // 1
复制代码

'use strict'
function test1 () {
    console.log(this)
}
test1.call(null) // null
test1.call(undefined) // undefined
test1.call(1) // 1
test1.apply(null) // null
test1.apply(undefined) // undefined
test1.apply(1) // 1
复制代码

  apply()有一个较为常见的用法:将数组转化成函数的参数序列。ES6中增长了扩展运算符“...”来实现该功能。以下代码所示:

var arr = [1,19,4,54,69,9]

var a = Math.max.apply(null,arr)
console.log(a) // 69

var b = Math.max(...arr)
console.log(b) // 69
复制代码

二、bind()

  bind()函数能够接收多个参数,返回一个功能相同、执行上下文肯定、参数通过初始化的函数。其中第一个参数为要绑定的执行上下文,剩余参数为返回函数的预约义值。bind()函数的做用有两点:一、为函数绑定执行上下文;二、进行函数柯里化。以下代码所示:

var a = 1

function func(b,c) {
    console.log(`a:${this.a},b:${b},c:${c}`)
}

var obj = {
    a: 2
}

var test = func.bind(obj,3)

test(4) // a:2,b:3,c:4
复制代码

  bind()方法是ES5加入的,可是咱们能够很轻易的在ES3中经过apply()模拟出来,下面代码是MDN上的bind()的polyfill。

if (!Function.prototype.bind) {
    Function.prototype.bind = function(oThis) {
        if (typeof this !== "function") {
            // 可能的与 ECMAScript 5 内部的 IsCallable 函数最接近的东西,
            throw new TypeError( "Function.prototype.bind - what " +
                "is trying to be bound is not callable"
            );
        }

        var aArgs = Array.prototype.slice.call( arguments, 1 ),
            fToBind = this,
            fNOP = function(){},
            fBound = function(){
                return fToBind.apply(
                    (this instanceof fNOP &&oThis ? this : oThis),
                    aArgs.concat( Array.prototype.slice.call( arguments ) )
                );
            };

        fNOP.prototype = this.prototype;
        fBound.prototype = new fNOP();

        return fBound;
    };
}
复制代码

5、规则的优先级

  函数的调用有时不仅一种,那么不一样调用方式的规则的优先级就最终决定了this的指向。那就让咱们来比较不一样调用方式的规则优先级。以下代码所示,当函数做为方法调用的时候,this指向调用方法的对象,看成为函数调用时,this指向在非严格模式下指向全局对象,在严格模式下指向undefined。所以,方法调用的优先级高于函数调用。

var a = 1

var obj = {
    a:2,
    test:test
}

function test () {
    console.log(this.a)
}

var b = obj.test

obj.test() // 2
b() // 1
复制代码

  以下代码所示是函数做为方法调用分别和间接调用、构造函数调用做对比。由代码可知:函数做为方法调用优先级分别小于间接调用和构造函数调用。

function test(para) {
    this.a = para
}

var obj1 = {
    test: test
}

var obj2 = {}
      
obj1.test( 2 )
console.log( obj1.a ) // 2

obj1.test.call( obj2, 3 )
console.log( obj2.a ) // 3

var bar = new obj1.test( 4 )
console.log( obj1.a ) // 2
console.log( bar.a ) // 4
复制代码

  new关键字后面是一个函数,而call()和apply()并非返回一个函数,而是依照传入参数来执行函数,所以形如new foo.call(obj)的代码是不被容许的。ES5中的bind()返回的是一个函数,能够与new关键字同时使用。以下代码所示,bind()返回的函数用做构造函数,将忽略传入bind()的this值,原始函数会以构造函数的形式调用,传入的参数也会原封不动的传入原始函数。

function test(something) {
    this.a = something;
}

var obj = {};

var bar = test.bind( obj );
bar( 2 );
console.log( obj.a ); // 2

var baz = new bar( 3 );
console.log( obj.a ); // 2
console.log( baz.a ); // 3
复制代码

  总之,构造函数的优先级大于间接调用,间接调用的优先级大于方法调用,方法调用的优先级大于函数调用。

6、词法this

  this关键字没有做用域限制,函数的this指向调用该函数的对象,在嵌套函数汇中,若是想访问外层函数的this值,能够将外层函数的this赋值给一个变量,用词法做用域来代替传统的this机制。以下代码所示:

function foo() {
    var self = this // 词法上捕获`this`
    setTimeout( function(){
        console.log( self.a )
    }, 1000 )
}

var obj = {
    a: 2
};

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

  ES6新增了箭头函数,箭头函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。以下代码所示,箭头函数可以将this固化,箭头函数内部没有绑定this的机制,其内部的this就是外层代码块的this。传统的this机制让不少人与词法做用域混淆,所以有了将this赋值给变量的行为,ES6只是将这种行为加以标准化而已。

var a = 21

function test() {
    setTimeout(() => {
        console.log('a:', this.a)
    }, 1000)
}

test.call({ a: 42 }) // 42
复制代码

7、总结

  JavaScript中的this机制跟词法做用域没有关系,根据函数调用的方式不一样,肯定this指向的规则也不相同。在肯定this指向时能够遵循如下步骤:

一、函数是否为构造函数调用,即函数跟在new关键字后面,若是是,this就是新构建的对象。
二、函数是否为间接调用,即经过call()、apply()或者bind()调用,若是是,this就是明确指定的对象。
三、函数是否为做为方法调用,即经过对象来调用函数,若是是,this就是该对象。
四、不然,即为做为函数的调用,在非严格模式下,this指向全局对象,在严格模式下,this为undefined。

  能够将外层函数的this赋值给一个变量,使得内层函数以词法做用域的规则来访问该this。ES6新增的箭头函数即是使用词法做用域来决定this绑定的。
如需转载,烦请注明出处:www.cnblogs.com/lidengfeng/…

相关文章
相关标签/搜索