当前位置:Gxlcms > JavaScript > Javascript中this绑定的3种方法与比较

Javascript中this绑定的3种方法与比较

时间:2021-07-01 10:21:17 帮助过:3人阅读

介绍

this 可以说是 javascript 中最耐人寻味的一个特性,学习this 的第一步就是明白 this 既不是指向函数自身也不指向函数的作用域。 this 实际上是在函数被调用时发生的绑定,它指向什么地方完全取决于函数在哪里被调用。

为什么需要绑定this

this代指当前的上下文环境,在不经意间容易改变:

  1. var info = "This is global info";
  2. var obj = {
  3. info: 'This is local info',
  4. getInfo: getInfo
  5. }
  6. function getInfo() {
  7. console.log(this.info);
  8. }
  9. obj.getInfo() //This is local info
  10. getInfo() //This is global info 当前上下文环境被修改了

在上面的例子中,我们在对象内部创建一个属性getInfo,对全局作用域下的getInfo进行引用,而它的作用是打印当前上下文中info的值,当我们使用obj.getInfo进行调用时,它会打印出对象内部的info的值,此时this指向该对象。而当我们使用全局的函数时,它会打印全局环境下的info变量的值,此时this指向全局对象。

这个例子告诉我们:

     1、同一个函数,调用的方式不同,this的指向就会不同,结果就会不同。

     2、对象内部的属性的值为引用类型时,this的指向不会一直绑定在原对象上。

其次,还有不经意间this丢失的情况:

  1. var info = "This is global info";
  2. var obj = {
  3. info: 'This is local info',
  4. getInfo: function getInfo() {
  5. console.log(this.info);
  6. var getInfo2 = function getInfo2() {
  7. console.log(this.info);
  8. }
  9. getInfo2();
  10. }
  11. }
  12. obj.getInfo();
  13. //This is local info
  14. //This is global info

上面的例子中,对象obj中定义了一个getInfo方法,方法内定义了一个新的函数,也希望拿到最外层的该对象的info属性的值,但是事与愿违,函数内函数的this被错误的指向了window全局对象上面,这就导致了错误。

解决的方法也很简单,在最外层定义一个变量,存储当前词法作用域内的this指向的位置,根据变量作用域的关系,之后的函数内部还能访问这个变量,从而得到上层函数内部this的真正指向。

  1. var info = "This is global info";
  2. var obj = {
  3. info: 'This is local info',
  4. getInfo: function getInfo() {
  5. console.log(this.info);
  6. var self = this; //将外层this保存到变量中
  7. var getInfo2 = function getInfo2() {
  8. console.log(self.info); //指向外层变量代表的this
  9. }
  10. getInfo2();
  11. }
  12. }
  13. obj.getInfo();
  14. //This is local info
  15. //This is local info

然而这样也会有一些问题,上面的self变量等于重新引用了obj对象,这样的话可能会在有些时候不经意间修改了整个对象,而且当需要取得多个环境下的this指向时,就需要声明多个变量,不利于管理。

有一些方法,可以在不声明类似于self这种变量的条件下,绑定当前环境下的上下文,确保编程内容的安全。

如何绑定this

1. call, apply

call和apply是定义在Function.prototype上的两个函数,他们的作用就是修正函数执行的上下文,也就是this的指向问题。

以call为例,上述anotherFun想要输出local thing就要这样修改:

  1. ...
  2. var anotherFun = obj.getInfo;
  3. anotherFun.call(obj) //This is local info

函数调用的参数:

      Function.prototype.call(context [, argument1, argument2 ])

      Function.prototype.apply(context [, [ arguments ] ])

从这里就可以看到,call和apply的第一参数是必须的,接受一个重新修正的上下文,第二个参数都是可选的,他们两个的区别在于,call从第二个参数开始,接受传入调用函数的值是一个一个单独出现的,而apply是接受一个数组传入。

  1. function add(num1, num2, num3) {
  2. return num1 + num2 + num3;
  3. }
  4. add.call(null, 10, 20, 30); //60
  5. add.apply(null, [10, 20, 30]); //60

当接受的context为undefined或null时,会自动修正为全局对象,上述例子中为window

2. 使用Function.prototype.bind进行绑定

ES5中在Function.prototype新增了bind方法,它接受一个需要绑定的上下文对象,并返回一个调用的函数的副本,同样的,它也可以在后面追加参数,实现函数的柯里化。

  1. Function.prototype.bind(context[, argument1, argument2])
  2. //函数柯里化部分
  3. function add(num1 ,num2) {
  4. return num1 + num2;
  5. }
  6. var anotherFun = window.add.bind(window, 10);
  7. anotherFun(20); //30

同时,他返回一个函数的副本,并将函数永远的绑定在传入的上下文中。

  1. ...
  2. var anotherFun = obj.getInfo.bind(obj)
  3. anotherFun(); //This is local info

polyfill

polyfill是一种为了向下兼容的解决方案,在不支持ES5的老旧浏览器上,如何使用bind方法呢,就得需要使用旧的方法重写一个bind方法。

  1. if (!Function.prototype.bind) {
  2. Function.prototype.bind = function (obj) {
  3. var self = this;
  4. return function () {
  5. self.call(obj);
  6. }
  7. }
  8. }

上面的写法实现了返回一个函数,并且将上下文修正为传入的参数,但是没有实现柯里化部分。

  1. ...
  2. Function.prototype.bind = function(obj) {
  3. var args = Array.prototype.slice.call(arguments, 1);
  4. //记录下所有第一次传入的参数
  5. var self = this;
  6. return function () {
  7. self.apply(obj, args.concat(Array.prototype.slice.call(arguments)));
  8. }
  9. }

当使用bind进行绑定之后,即不能再通过call,apply进行修正this指向,所以bind绑定又称为硬绑定。

3. 使用new关键字进行绑定

在js中,函数有两种调用方式,一种是直接进行调用,一种是通过new关键字进行构造调用。

  1. function fun(){console.log("function called")}
  2. //直接调用
  3. fun() //function called
  4. //构造调用
  5. var obj = new fun() //function called

那普通的调用和使用new关键字的构造调用之间,又有哪些区别呢?

准确的来说,就是new关键字只是在调用函数的基础上,多增加了几个步骤,其中就包括了修正this指针到return回去的对象上。

  1. var a = 5;
  2. function Fun() {
  3. this.a = 10;
  4. }
  5. var obj = new Fun();
  6. obj.a //10

几种绑定方式的优先级比较

以下面这个例子来进行几种绑定状态的优先级权重的比较

  1. var obj1 = {
  2. info: "this is obj1",
  3. getInfo: () => console.log(this.info)
  4. }
  5. var obj2 = {
  6. info: "this is obj2",
  7. getInfo: () => console.log(this.info)
  8. }

1. call,apply和默认指向比较

首先很显然,根据使用频率来想,使用call和apply会比直接调用的优先级更高。

  1. obj1.getInfo() //this is obj1
  2. obj2.getInfo() //this is obj2
  3. obj1.getInfo.call(obj2) //this is obj2
  4. obj2.getInfo.call(obj1) //this is obj1

使用call和apply相比于使用new呢?

这个时候就会出现问题了,因为我们没办法运行类似 new function.call(something)这样的代码。所以,我们通过bind方法返回一个新的函数,再通过new判断优先级。

  1. var obj = {}
  2. function foo(num){
  3. this.num = num;
  4. }
  5. var setNum = foo.bind(obj);
  6. setNum(10);
  7. obj.num //10
  8. var obj2 = new setNum(20);
  9. obj.num //10
  10. obj2.num //20

通过这个例子我们可以看出来,使用new进行构造调用时,会返回一个新的对象,并将this修正到这个对象上,但是它并不会改变之前的对象内容。

那么问题来了,上面我们写的bind的polyfill明显不具备这样的能力。而在MDN上有一个bind的polyfill方法,它的方法如下:

  1. if (!Function.prototype.bind) {
  2. Function.prototype.bind = function (oThis) {
  3. if (typeof this !== "function") {
  4. throw new TypeError("Function.prototype.bind -
  5. what is trying to be bound is not callable");
  6. }
  7. var aArgs = Array.prototype.slice.call(arguments, 1),
  8. fToBind = this,
  9. fNOP = function () {},
  10. fBound = function () {
  11. return fToBind.apply(this instanceof fNOP ?
  12. this : oThis || this,
  13. aArgs.concat(Array.prototype.slice.call(arguments)));
  14. };
  15. fNOP.prototype = this.prototype;
  16. fBound.prototype = new fNOP();
  17. return fBound;
  18. };
  19. }

上面的polyfill首先判断需要绑定的对象是否为函数,防止使用Function.prototype.bind.call(something)时,something不是一个函数造成未知错误。之后让需要返回的fBound函数继承自this,返回fBound。

特殊情况

当然,在某些情况下,this指针指向也存在一些意外。

箭头函数

ES6中新增了一种定义函数方式,使用"=>"进行函数的定义,在它的内部,this的指针不会改变,永远指向最外层的词法作用域。

  1. var obj = {
  2. num: 1,
  3. getNum: function () {
  4. return function () {
  5. //this丢失
  6. console.log(this.num);
  7. //此处的this指向window
  8. }
  9. }
  10. }
  11. obj.getNum()(); //undefined
  12. var obj2 = {
  13. num: 2,
  14. getNum: function () {
  15. return () => console.log(this.num);
  16. //箭头函数内部绑定外部getNum的this,外部this指向调用的对象
  17. }
  18. }
  19. obj2.getNum()(); //2

软绑定

上面提供的bind方法可以通过强制修正this指向,并且再不能通过call,apply进行修正。如果我们希望即能有bind效果,但是也能通过call和apply对函数进行二次修正,这个时候就需要我们重写一个建立在Function.prototype上的方法,我们给它起名为"软绑定"。

  1. if (!Function.prototype.softBind) {
  2. Function.prototype.softbind = function (obj) {
  3. var self = this;
  4. var args = Array.prototype.slice.call(arguments, 1);
  5. return function () {
  6. return self.apply((!this || this === (window || global)) ?
  7. obj : this,
  8. args.concat(Array.prototype.slice.call(arguments)));
  9. }
  10. }
  11. }

bind,call的妙用

在平日里我们需要将伪数组元素变为正常的数组元素时,往往通过Array.prototype.slice方法,正如上面的实例那样。将arguments这个对象变为真正的数组对象,使用 Array.prototype.slice.call(arguments)进行转化.。但是,每次使用这个方法太长而且繁琐。所以有时候我们就会这样写:

  1. var slice = Array.prototype.slice;
  2. slice(arguments);
  3. //error

同样的问题还出现在:

  1. var qsa = document.querySelectorAll;
  2. qsa(something);
  3. //error

上面的问题就出现在,内置的slice和querySelectorAll方法,内部使用了this,当我们简单引用时,this在运行时成为了全局环境window,当然会造成错误。我们只需要简单的使用bind,就能创建一个函数副本。

  1. var qsa = document.querySelectorAll.bind(document);
  2. qsa(something);

同样的,使用因为call和apply也是一个函数,所以也可以在它们上面调用bind方法。从而使返回的函数的副本本身就带有修正指针的功能。

  1. var slice = Function.prototype.call.bind(Array.prototype.slice);
  2. slice(arguments);

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或工作能带来一定的帮助,如果有疑问大家可以留言交流。

人气教程排行