读懂JS核心(四)–this问题

this的指向

这一节,我们会对JS的this指向做详细的介绍,给大家介绍不同情况下this是如何指向的,还会给大家补充在es5和es6中我们是如何利用this的,帮助大家更好的理解JS的指向问题。

ES5中的this指向问题

在ES5中,this的指向其实只有一个原理:this永远指向最后调用它的对象。我们来看几个例子:

// 例1
var name = "windowsName";
function a() {
    var name = "Cherry";

    console.log(this.name); // windowsName
    console.log("inner:" + this); // inner: window
}
a();
console.log("outer:" + this); // outer: window
复制代码

例1中,最终调用函数a的是window对象,这里其实等同于window.a()。所以this指向window对象。

// 例2
var name = "windowsName";
var a = {
    name: "Cherry",
    fn : function () {
        console.log(this.name);      // Cherry
    }
}
a.fn();
复制代码

例2中,在对象a中声明了一个名为fn的函数,因为调用fn的对象是a,所以这里的this指向对象a。

// 例3
var name = "windowsName";
var a = {
    name: "Cherry",
    fn : function () {
        console.log(this.name);      // undefined
    }
}
window.a.fn();
复制代码

例3中,最终调用函数fn的对象是a,所以this指向a。

// 例4
var name = "windowsName";
var a = {
    name : null,
    // name: "Cherry",
    fn : function () {
        console.log(this.name);      // windowsName
    }
}

var f = a.fn;
f();
复制代码

例4中,在window上声明了一个变量f,指向函数fn,所以最终指向fn的是window对象。

怎么改变this的指向

改变 this 的指向,总结下来有以下几种方法:

  • 使用 ES6 的箭头函数

  • 在函数内部使用 _this = this

  • 使用 apply、call、bind

  • new 实例化一个对象

// 例5
var name = "windowsName";

var a = {
    name : "Cherry",

    func1: function () {
        console.log(this.name)     
    },

    func2: function () {
        setTimeout(function () {
            this.func1()
        },100);
    }

};

a.func2()     // this.func1 is not a function
复制代码

例5中,我们调用了对象a上的func2函数,想让它在100毫秒后执行this.func1(),却得到了this.func1 is not a function的结果。原因是func2中100毫秒后执行的匿名函数并没有指定它所指向的this,所以默认会指向window对象。而window对象上并没有名为func1的函数,所以报错。

我们以例5作为demo,用上面提到的各种方法进行改造,使得上述代码得到预期的结果。

箭头函数

箭头函数是在ES6出现的,使用它可以避免ES5中使用this的坑。箭头函数的原理其实很简单:箭头函数的this始终指向函数定义时的this,而非执行时的

我们来把例5改写一下:

var name = "windowsName";

var a = {
    name : "Cherry",

    func1: function () {
        console.log(this.name)     
    },

    func2: function () {
        setTimeout(() => {
            this.func1()
        },100);
    }

};

a.func2()     // Cherry
复制代码

上述代码中,我们把func2中的setTimeout函数中的执行函数由原来的匿名函数改为了箭头函数。箭头函数的this始终指向创建定义它的this,即func2,也就是说,箭头函数的this指向就是func2的this指向。这里对象a调用了func2,所以func2的this指向a,箭头函数中的this也指向a。

我们把上面的调用改一下:

...
const funcA = a.func2;
funcA(); // this.func1 is not a function
复制代码

这时我们发现结果又报错了,这是为什么呢?原因就在于func2的this指向。我们知道:箭头函数的this指向与创建它的函数(即func2)的this指向是相同的。但是在刚才的例子中,我们创建了一个变量funcA,指向对象a中的函数func2,然后调用funcA。这时func2的this指向的是window对象,而非对象a。

不信我们再改写一下:

var a = {
    name : "Cherry",

    func2: function () {
        setTimeout(() => {
            console.log(this);
        },100);
    }

};

const funcB = a.func2;
funcB(); // window
复制代码

所以说,箭头函数的this还是需要依赖于创建它时的外层的this。

当箭头函数内部嵌套箭头函数时呢?我们来看一个例子:

var a = {
    name : "Cherry",
    
    func1: function () {
        console.log(this.name)     
    },

    func2: function () {
        setTimeout(() => {
            const fn = () => {
                this.func1();
            };
            fn()
        },100);
    }
};
a.func2(); // Cherry;
复制代码

我们会发现:当箭头函数嵌套时,内层的箭头函数的this指向 与 外层的箭头函数的this指向 相同

声明变量指向当前this对象

我们可以通过声明一个变量_this,来保存当前函数的this值,然后在函数调用时使用_this来代替this,从而使得this指向不根据调用它的对象发生变化。

var name = "windowsName";

var a = {
    name : "Cherry",

    func1: function () {
        console.log(this.name)     
    },

    func2: function () {
        const _this = this;
        setTimeout(function () {
            _this.func1()
        },100);
    }

};

a.func2() // Cherry;
复制代码

这里func2中声明了一个变量_this,_this的指向与func2的this指向相同。当我们调用匿名函数来寻找_this.func1()时,它的指向不再是window对象,而是func2的this指向的对象(即对象a)。

使用call、apply、bind

call、apply、bind是ES5中提供的几种用来修改this指向的方法。他们的效果是相同,但用法存在差异。

  • call

fun.call(thisArg[, arg1[, arg2[, …]]])

  • apply

fun.apply(thisArg, [argsArray])

  • bind(this, arg1, arg2, …)

bind参数接收与call相同,但是返回的是一个函数,而不是去执行这个函数。需要手动调用

我们会发现:其实apply与bind都与call方法相似,apply的不同点在于参数的传递,而bind的不同点在于返回值是一个函数。

我们来用call、apply、bind来分别改写例5,来让它的this指向正确的值:

// call
var a = {
    name : "Cherry",

    func1: function () {
        console.log(this.name)     
    },

    func2: function () {
        setTimeout(function () {
            this.func1();
        }.call(a),100);
    }

};

// apply
var a = {
    name : "Cherry",

    func1: function () {
        console.log(this.name)     
    },

    func2: function () {
        const _this = this;
        setTimeout(function () {
            this.func1();
        }.apply(a),100);
    }

};

// bind
var a = {
    name : "Cherry",

    func1: function () {
        console.log(this.name)     
    },

    func2: function () {
        const _this = this;
        setTimeout(function () {
            this.func1();
        }.bind(a)(),100);
    }

};
复制代码

我们再来看看这三个方法的参数的传递,下面有一个求和的小例子:

// 例6
var obj = {
    init: 10,
    fn: function (a,b) {
        return a + b + this.init;
    }
}

var obj2 = {
    init: 100,
}

const fn = obj.fn;
fn(1,2); // NaN
复制代码

小伙伴们应该很快就能想明白:最终调用fn的对象是window,而window上是没有init的,所以会输出NaN。我们来用call、apply、bind来修改this的指向:

// call
fn.call(obj, 1, 2) // 13
fn.call(obj2, 1, 2) // 103

// apply
fn.apply(obj, [1, 2]) // 13
fn.apply(obj2, [1, 2]) // 103

// bind
fn.bind(obj, 1, 2)() // 13
fn.bind(obj2, 1, 2)() // 103
复制代码

通过call、apply、bind,我们就可以指定this的指向,让函数正确执行啦!

使用构造函数调用函数

我们可以通过new关键字来调用构造函数,我们先来了解一下new是如何创建一个新的对象的,即new的创建过程

  1. 创建一个新对象

  2. 新对象继承原函数的原型

  3. 将这个新对象绑定到这个函数的this上

  4. 如果这个函数没有返回其它对象,则返回这个新对象

让我们来手写一个new操作符吧!

// create为一个构造函数
function create(Con, ...arguments) {
    // 创建一个新对象
    let obj = {};
    // 新对象继承原函数的原型
    Object.setPrototypeOf(obj, Con.prototype);
    // 通过apply修改原函数的this指向,并执行
    let result = Con.apply(obj, arguments);
    // 如果执行结果为object,则认为函数返回了对象,否则返回新对象obj
    return result instanceof Object ? result : obj;
}

function Obj(name, age) {
    this.name = name;
    this.age = age;
}
Obj.prototype.sayName = function() {
    console.log(this.name);
}
const obj = new create(Obj, 'li', 18)
复制代码

总结

在这一节,我们详细介绍了JS中this的指向问题,解释了箭头函数, apply、call、bind的区别及使用。最后,我们还补充了new的过程。接下来我们会继续介绍其他JS核心知识。

我是何以庆余年,如果文章对你起到了帮助,希望可以点个赞,谢谢!

如有问题,欢迎在留言区一起讨论。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享