八.一篇文章搞定JavaScript中this指向问题

827 阅读11分钟

this取值是在函数执行是确定的:

  • 1.普通函数 ://window
  • 2.普通函数的call方法fn.call({name:1})://{name:1}
  • 3.普通函数的bind方法fn.bind({name:1})://{name:1}

const fn2= fn1.bind({name:1})
fn2()//bind会返回一个新的函数执行

  • 4.对象的方法执行,this指向该对象 obj.say()
  • 5.setTimeout(function(){console.log(this)},2000)function里面的this指向window
  • 6.setTimeout(()=>{},2000)指向外面的对象
  • 7.class中的指向实例本身

第一部分:常见的this指向问题

1. 全局上下文中的 this

在浏览器环境中,全局上下文中的 this,就是 window。

let a = 12;
console.log(this); // 控制台输出为 window

2. 块级上下文中的 this

  • 块级上下文中没有自己的 this,它的 this 是继承所在上级上下文的 this。
  • this是执行主体,不是执行上下文;执行上下文是EC ; ( 比如我在北京饭店吃饭,我就是this,北京饭店是执行上下文).
let obj = {
  fn() {
    // fn函数中的this是obj
    {
      let a = 12;
      console.log(this); // 输出obj。继承上级上下文,即函数fn的私有上下文中的this
    }
  },
};
obj.fn();

3. 给事件绑定方法中的this

函数私有上下文中的 this 千变万化,可以总结为以下五种情况:

3.1 事件绑定

给元素的某个事件行为绑定方法,事件触发时方法执行,此时方法中的 this一般为当前元素本身

let body = document.body;
//DOM0中事件绑定
body.onclick = function () {
  console.log(this); // body
};
3.2 在 element 上绑定事件
<div class="container">
  <div id="aa" onclick="clicke()"></div>
</div>;

function clicke() {
  console.log(this); // Window
}
3.3 js 绑定 onclick 事件

this 指向该元素

<div class="container">
  <div id="aa" onclick="clicke()"></div>
</div>;

document.getElementById("aa").onclick = function () {
  console.log(this); //  <div id="aa"></div>
};
3.4 js 使用 addEventListener 绑定事件

此时的 this 指向 该元素, 注意: 在 IE 浏览器中,使用为 attachEvent(), this 指向全局变量

<div class="container">
  <div id="aa" onclick="clicke()"></div>
</div>;

document.getElementById("aa").addEventListener("click", function () {
  console.log(this); //  <div id="aa"></div>
});
3.5 jquery 的 3 种绑定 click 事件

此时的 this 均指向该元素

$("#aa").bind("click", function () {
  console.log(this); //  <div id="aa"></div>
});

$("#aa").click(function () {
  console.log(this); //  <div id="aa"></div>
});

$("#aa").on("click", function () {
  console.log(this); //  <div id="aa"></div>
});

4. 普通函数执行中的this

这里的普通方法包含:普通函数、自执行函数、对象成员访问调取方法执行…等。 只要看函数执行时,方法名前是否有“点”就是了。 如果有“点”——xxx.方法()中 this 就是 xxx;如果没有“点”——this 即为 window(严格模式下为 undefined)。

// 自执行函数
(function(){
	console.log(this); // 非严格模式下输出window,严格模式下输出undefined
})();

let obj = {
	fn: (function(){
		console.log(this);    // 输出window
		return function(){};
	})();
}
/*
上面两个例子都是自执行函数执行,输出均为window。说明this的指向,与函数在哪
定义和执行的无关。自执行函数中的this一般都为window(因为函数执行时前面没有“点”)
*/
function func(){
	console.log(this);
}
let obj = {
	func:func
}

// 普通函数执行
func();            // this:window
// 成员访问方法执行
obj.func();        // this:obj


[].slice()方法中this->当前空数组。
Array.prototype.slice()方法中的this->Array.prototype

涵盖以上两种情况的一道题: 点击body输出什么?

function func(){
	console.log(this);//Window
}
document.body.onclick = function(){
    console.log(this)//HTMLBodyElement
	func();
}
/*
body
window
*/

5 构造函数执行的this

构造函数体中的 this 指向的就是当前类的实例。因此,函数中出现的 this.xxx = xxx 这样的代码就是给当前实例设置私有属性;
换句话说,类中凡是不带 this 的代码统统都与它创建的实例无关,充其量也就是这个函数私有上下文中的私有变量而已。

function Func() {
  console.log(this); 
  // 构造函数执行,构造函数体中的this指向当前类的实例——f,因此输出的this为f
}
Func.prototype.getNum = function getNum() {
  console.log(this);
};
let f = new Func();
//原型上方法中的this不一定是实例,要看执行时前面有没有点( 即当作普通函数看待 )
f.getNum(); //f
f.__proto__.getNum(); //f.__proto__
Func.prototype.getNum(); //Func.prototype

6. ES6 中的箭头函数

普通函数执行过程:箭头函数执行:
形成私有上下文和(AO)
初始化作用域链
初始化 this
初始化 arguments
形参赋值
变量提升
代码执行
形成私有上下文和(AO)
初始化作用域链
形参赋值
变量提升
代码执行
  • 箭头函数没有自己的 this,它的 this 继承自上级上下文中的 this===>这一点和块级上下文类似。
let obj = {
	func:function(){
		console.log(this);
	}
	sum:()=>{
		console.log(this);
	}
}
obj.func();       // this:obj 对象成员访问
obj.sum();        // this:window
obj.sum.call(obj);    // this:window 箭头函数没有this,改也没用

7. 回调函数中的 this

回调函数中的 this 一般指向 window,但也有例外;、

      let obj = {
        i: 0,
        func() {
          console.log("func", this); //obj
          setTimeout(function () {
            console.log("定时器里面的", this); //window
          }, 1000);
        },
      };
      obj.func();

如何改写 this? 1.赋值that=this

let obj = {
	i:0,
    let that=this;
	func(){
		setTimeout(function(){
			that.i++;
			console.log(that);    // {i:1,func:f}
		},1000);
	}
}
obj.func();

2.用bind强行改 function () {}.bind(this)

let obj = {
  i: 0,
  func() {
    setTimeout(
      function () {
        this.i++;
        console.log(obj); // {i:1,func:f}
      }.bind(this),
      1000
    ); // 这里强行让内部这个回调函数中的this指向外层的this(即obj)
  },
};
obj.func();

3.用箭头函数

//第三种解决方案:回调函数用箭头函数,箭头函数中没有自己的this,这里用的this是继承自外层的,也就是func函数的this->obj!
let obj = {
  i: 0,
  func() {
    setTimeout(() => {
      this.i++;
      console.log(obj); // {i:1,func:f}
    }, 1000);
  },
};
obj.func();

第二部分:改变this指向

  • call/apply/bind 方法强制手动改变函数中的 this 指向
  • 这三个方法都在函数类的原型对象 Function.prototype 上,因此所有的函数都能基于原型链proto找到并调用这三个方法 ;

2.1 call

[function].call([context],params1,params2,…)

含义:执行 call 方法时,会将后面的参数传递给[function]执行,并把函数中的 this 修改为[context]:
@ [function]: 希望改变自身 this 的那个函数
@ [context]: 希望把 this 变为它!
@ params1, params2, … : 传递给函数的参数

第一个参数不传 / null / undefined时:
非严格模式——thiswindow
严格模式——传谁this就是谁,不传就是undefined

2.2 apply

与 call 方法相同,只不过传递函数实参以数组的方式。

[function].call([context],[params1,params2,…])

2.3 bind

bind 的语法与 call 相同,作用与以上两个方法均不同。 它是预先修改 this,预先存储参数,而函数不被立即执行; 也就是说,call / apply 都是把函数立即执行的,并且改变 this。

[function].call([context],params1,params2,…)

2.4案例分析

需求:把func函数绑定给body的click事件,触发body的click事件时,func函数执行要让func函数中的this指向obj,并给func函数传递参数10,20; 方案一:不可行

document.body.onclick = func.call(obj, 10, 20);
/*
不可取func.call(obj,10,20)会立即执行func函数(并修改this&传参),
它哪管你什么时候点击body。这种写法实际上func.call(obj,10,20)执行的结果作为值绑定给body点击事件。
*/

方案二:可行

document.body.onclick = function anonymous() {
  func.call(obj, 10, 20);
};
/*
将一个匿名函数绑定给body的点击事件,也就是将这个匿名函数的地址绑定给了事件,
并没有立即执行。当点击事件触发时,将匿名函数执行,然后执行func.call。
事实上,在bind方法之前,大家就是这样处理的。已经和bind的原理非常类似了。
*/

方案三:可行

document.body.onclick = func.bind(obj, 10, 20);
/*
bind函数不会立即执行,而是预先修改this,并预先存储需要传递的参数。后续需要时(事件触发)再执行函数。
*/

2.5 bind 原理

      function func(x, y) {
        console.log(this, x, y); //改了之后就是obj
      }

      Function.prototype.bind1 = function bind1(context = window, ...params) {
 /*
 1.bind方法执行的时候,预先修改this,预先存储参数,而函数不被立即执行,所以要
 返回一个函数特定时(比如点击之后)候执行
*/
//2.bind执行的时候,因为是func调用的所以this指向func
        console.log("bind函数里面的this:", this); //func
//3.当点击事件时会执行bind()的返回结果,因为func.bind()执行后返回一个函数,被onclick绑定了
        let that = this;
        return function anonymous() {
          //匿名函数的this指向window的,要改变func的this,让that=this
          console.log("匿名函数的this", this); //window
          //改变this,并执行函数
          that.call(context, ...params);
        };
      };
      let obj = {};
      onclick = func.bind1(obj, 10, 20);
      /*
第一步:bind传参设置:如果不传参,this默认为改为window,后面的参数用展开运算符接受
第二步:特定条件下,bind才会改变this指向,比如点击了才改变this指向,而func.bind1(obj, 10, 20);时立即执行的,所以他的执行结果时返回一个函数,让点击事件绑定
第三步:bind执行的时候,因为是func调用的所以this指向func;而返回的供点击事件执行的匿名函数他的this是window;
第四步:bind函数里面让that=this,然后在匿名函数里面执行that.call()也就是func.call()
      */

有点乱,简化一下:

      function func(x, y) {
        console.log(this, x, y); //改了之后就是obj
      }
      Function.prototype.bind1 = function bind1(context = window, ...params) {
        let that = this;
        return function anonymous() {
          that.call(context, ...params);
        };
      };
      let obj = {};
      onclick = func.bind1(obj, 10, 20);

利用 apply实现bind方法 改变 this 指向:

Function.prototype.bind = function bind(context = window, ...params) {
  //这是bind函数的this指向func,因为调用了func.bind
  var that = this;
  return function (...inners) {
    //this指向body
    that.apply(context, params.concat(inners));
  };
};
body.onclick = func.bind(obj, 10, 20, 30);
/*
利用闭包机制预先把需要执行的函数以及改变的this以及后续需要给函数传递的参数信息
都保存到不释放的上下文中,后续使用的时候直接拿出来用
*/

2.6 call,apply 应用

把类数组转化为数组

类数组为对象,但有一些数组的方法,用起来像数组,有索引,length,和可迭代性;

常见的有:函数实参集合,dom 元素集合,dom 节点集合 把类数组转化为数组就可以用数组原型上的方法 1.Array.from()

      function toArray() {
        console.log(arguments);
        arguments = Array.from(arguments);
        console.log(arguments); //数组
      }
      toArray(1, 2, 3, 4, 5, 6);

2....展开运算符

      function toArray(...arguments) {
        console.log(arguments);//[1, 2, 3, 4, 5, 6]
      }
      toArray(1, 2, 3, 4, 5, 6);

3.手动循环

      function toArray() {
        let args = [];
        for (let i = 0; i < arguments.length; i++) {
          args.push(arguments[i]);
        }
        console.log(args); // [1, 2, 3, 4, 5, 6]
      }
      toArray(1, 2, 3, 4, 5, 6);

4.arguments具备和数组类似的结构,所以操作数组的一些代码(例如:循环)也同样适用于arguments;
如果我们让Array原型上的内置方法执行,并且让方法中的this变为我们要操作的类数组,那么就相当于我们在“借用数组原型上的方法操作类数组”,让类数组也和数组一样可以调用这些方法实现具体的需求:

      function toArray() {
        // let args = Array.prototype.slice.call(arguments);
        let args = [].slice.call(arguments);
        console.log(args); //[1, 2, 3, 4, 5, 9, 6, 7]
        // 借用array.prototype.forEach,让forEach中的this指向arguments
        [].forEach.call(arguments, (item) => {
          console.log(item);
        });
      }
      toArray(1, 2, 3, 4, 5, 9, 6, 7);

改变this指向:

      function func(x, y) {
        console.log(this, x, y); //改了之后就是obj
      }
      Function.prototype.myBind = function () {
        //将参数拆解为数组,并把this指向数组原型
        const args = Array.prototype.slice.call(arguments);
        //获取this(数组第一项)
        const t = args.shift();
        const self = this;
        //返回一个this已经改变了的函数
        return function () {
          return self.apply(t, args);
        };
      };
      let obj = {};
      document.body.onclick = func.myBind(obj, 10, 20, 30);

第三部分:补充一些阶段训练题

练习一:

 var x = 3;
            var obj = {
              x: 5,
            };
            //自执行函数this是window
            obj.fn = (function () {
              //自己执行window.x=window.x*(++x)=3*4=12
              this.x *= ++x;
              return function (y) {
                this.x *= ++x + y;
                console.log(x);
              };
            })();
            var fn = obj.fn;
            obj.fn(6);
            fn(4);
            console.log(obj.x, x);
/*-------------------------------------------------------------------------------------------*/
//解析过程

          ec(g)全局上下文:
                           AF1对象堆{
                                     x: 5,==>x=95
                                 }

                          Bf1函数堆{
                                  形参:y
                                  this.x *= ++x + y;
                                  console.log(x);
                                }


               变量提升:x=3=>x=12=>x=13=》x=14=》x=234 obj=Af1对象堆
               代码执行:
                      obj.fn =()() =Bf1
                      fn = obj.fn=Bf1
                      obj.fn(6);
      ec(Ofn1)==ec(bf1)私有上下文:{
        原型链:ec(fn1)-->ec(g)
        初始化thisthis-->obj
        形参赋值:y=6
        变量提升:
        代码执行:this.x *= ++x + y;//obj.x=obj.x*(++x+y)=5*(13+6)=19*5=95
                 console.log(x);//打印x=13

      }
                    fn(4);
      ec(fn2)=bf1(fn2)私有上下文:{
        原型链:ec(fn2)-->ec(g)
        初始化thisthis-->window
        形参赋值:y=4
        变量提升:
        代码执行:this.x *= ++x + y;//window.x=window.x*(++x+y)=13*(14+4)=234
                 console.log(x);//打印x=234
                 }

                      console.log(obj.x, x);//打印95  234

练习二:

let obj = {
  fn: (function () {
    return function () {
      console.log(this);
    };
  })(),
 /*
        fn:function(){
           console.log(this);
        }
        
 */
};
obj.fn(); //this指向obj打印obj
let fn = obj.fn;
fn(); //window

练习三:

var fullName = "language";
var obj = {
  fullName: "javascript",
  prop: {
    getFullName: function () {
      return this.fullName;
    },
  },
}; //af0对象堆
console.log(obj.prop.getFullName()); //obj.prop.fullname=undefined
var test = obj.prop.getFullName; //test=function(){  return this.fullName}
console.log(test()); //this是全局打印language

练习四:

var name = "window";
var Tom = {
  name: "Tom",
  show: function () {
    console.log(this.name);
  },
  wait: function () {
    // this:Tom
    var fun = this.show;
    /*
       fun=function () {
          console.log(this.name);
        },
    */
    fun(); // this:window  => window.name => 'window'
  },
};
Tom.wait(); //第一步:this指向tom

练习题五:

window.val = 1;
var json = {
  val: 10,
  dbl: function () {
    this.val *= 2;
  },
};
json.dbl();
/*
       this:json
       json.val = json.val * 2 = 20
*/
var dbl = json.dbl;
dbl();
// this:window
// window.val = window.val * 2 = 2

json.dbl.call(window);
// this:window
// window.val = window.val * 2 = 4

alert(window.val + json.val); //=>'24'

练习六:

(function () {
  var val = 1; //val=2 变量 【注意,这个变量不是全局的,而是匿名函数私有上下文中的】
  var json = {
    val: 10, // 属性 【不是变量】
    dbl: function () {
      // this:json
      val *= 2; // val=val*2=1*2=2
    },
  };
  json.dbl(); //函数执行形成栈他的上级上下文是不是json,把val=2
  alert(json.val + val); //=>'12'
})();

练习七:箭头函数与普通函数的区别

箭头函数中没有arguments
箭头函数中没有this,(箭头函数的this看外层的是否有函数,如果有,外层函数的this就是内部箭头函数的this, 如果没有,则thiswindow。)
箭头函数没有构造函数(没有原型prototype),所以不能被new执行

·