js中的this机制

251 阅读4分钟

这周来学习this,this虐我千百遍,我待this如初恋,每次一看this就会,就是指向它的调用者嘛,一做题就废,这次挖挖根,彻底了解一下this的机制

由来

为什么要用this

不使用this,需要显示的传递一个上下文对象

function identify(person){
  return person.name
}
​
function speak(person){
  var greeting = "Hello , I am " + identify(person)
  console.log(greeting)
}
​
const p1={
  name:"ABC"
}
const p2={
  name:"DEF"
}
​
identify(p1) // ABC
speak(p2) // Hello , I am DEF

使用this,可以隐式传递对象引用,因此可以将API设计的更加简洁和易于复用

function identify(){
  return this.name
}
const p1={
  name:"ABC"
}
​
identify.call(p1) // ABC

this的误解

  1. this指向自身(这是错误的!!!)

    function fn(num){
      console.log(num)
      this.count++
    }
    fn.count = 0;
    for(var i=0;i<5;i++){
      fn(i)
    }
    console.log(fn.count) //0====>?????
    

    按照this指向自身的说法,那么this.count++ 相当于 foo.count++ 实际输出的应该是5,但是最终输出的是0,显然这样理解是错误的。

    解决方案

    • 创建一个全局的count属性(利用了全局词法作用域,回避了this)

      var count =0;
      function fn(){
        this.count++
      }
      for(var i=0;i<5;i++){
        fn(i)
      }
      console.log(this.count) //5
      
    • 创建一个函数count属性(利用了变量fn的词法作用域,回避了this)

      function fn(){
        fn.count++
      }
      fn.count =0;
      for(var i=0;i<5;i++){
        fn(i)
      }
      console.log(fn.count) //5
      
    • 强制this指向fn

      function fn(){
        this.count++
      }
      fn.count=0;
      for(var i=0;i<5;i++){
        fn.call(fn,i)
      }
      console.log(fn.count) //5
      
  2. this的作用域

    this指向函数的作用域,这是错误的。在任何情况下this都不指向函数的词法作用域

    function foo(){
      var a = 2;
      this.bar()
    }
    function bar(){
      console.log(a)
    }
    foo() //Uncaught ReferenceError: a is not defined
    

    词法作用域

    window
      foo
        a:
        bar:function
    

    每当你想要把this和词法作用域的查找混合使用是,一定要提醒自己,这是无法实现的。

  3. this到底是什么

    this是在运行时进行绑定的,它的上下文取决于函数调用时的各种条件。

    this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。

    this既不指向函数自身也不指向函数的词法作用域。

    回顾一下JavaScript执行过程学习笔记,js在执行阶段(函数调用)进行this的绑定(即运行时绑定)

全貌解析

调用位置

调用位置:就是函数在代码中被调用的位置(不是函数声明的位置)

function baz(){
  console.log('baz')
  bar()
}
function bar(){
  console.log('bar')
  foo()
}
function foo(){
  debugger
  console.log('foo')
}
baz();

调用栈如图中Call Stack 此时this指向window

截屏2021-08-03 下午8.01.28.png

const obj = {
	name:'测试',
	baz:function(){
	 		console.log('baz')
  	  this.bar()
	},
	bar:function(){
	 		console.log('bar')
  	  this.foo()
	},
	foo:function(){
	 		console.log(foo)
	}
}

obj.baz()

调用栈如图中Call Stack 此时this指向obj

截屏2021-08-04 下午1.33.37.png

const person = {
	name:'ddd'
}
function speak(){
	console.log(this.name)
}
speak();// this 指向window
speak.call(person) // this 指向person

绑定规则

函数在执行过程中调用位置如何决定this的指向呢?

1.默认绑定

独立函数调用

  • 非严格模式,this指向window

    function foo(){
      console.log(this.a) 
    }
    var a = 2;
    foo()//2
    
  • 严格模式,this 指向undefined

    function foo(){
        "use strict"
      console.error(this.a) 
    }
    var a = 2;
    foo()//VM156:3 Uncaught TypeError: Cannot read property 'a' of undefined
    

2.隐式绑定

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

function foo(){
  console.log(this.a)
}
var a = 'window'var obj ={
  a:'obj',
  foo:foo
}
obj.foo() //'obj'

调用位置使obj上下文来引用函数,隐式绑定规则把this指向当前的上下文对象obj

对象属性调用链中,只有上一次或者说最后一层在调用位置中起作用

function foo(){
  console.log(this.a)
}
var a = 'window'var obj1 ={
  a:'obj1',
  foo:foo
}
var obj2 ={
  a:'obj2',
  obj1:obj1,
  foo:foo
}
obj2.obj1.foo() // 'obj1'

隐式丢失问题

  • 赋值操作,bar = obj1.foo 实际是对foo函数的引用,bar调用时不含任何修饰符,隐藏应用默认绑定规则

    function foo(){
      console.log(this.a)
    }
    var a = 'window'var obj1 ={
      a:'obj1',
      foo:foo
    }
    var bar = obj1.foo
    bar() // window
    
  • 函数作为参数传递,参数传递实际是一种隐式赋值,doFn(obj.foo) 实际 var fn = obj.foo; fn() ; fn调用时不含任何修饰符,隐藏应用默认绑定规则

    function foo(){
      console.log(this.a)
    }
    var a = 'window'var obj ={
      a:'obj',
      foo:foo
    }
    function doFn(fn){
      fn()// 调用位置
    }
    ​
    doFn(obj.foo) // window
    
  • 回调函数this丢失

    function foo(){
      console.log(this.a)
    }
    var a = 'window'var obj ={
      a:'obj',
      foo:foo
    }
    setTimeout(obj.foo,100) //'window'
    

    setTimeout 内部实现伪代码,函数调用时实际是把 obj.foo赋值给fn,fn实际是指向foo自身函数

    function setTimeout(fn,delay){
      // 延迟时间
      fn() // 实际调用位置
    }
    

3.显示绑定

回顾一下上面的隐式绑定,我们必须在一个对象内包含一个指向函数的属性,并且通过对象这个属性来间接引用来调用函数,从而把this隐式绑定到这个上下文对象上,那么如果我们不通过对象内部包含函数的方式,而想在对象上强制调用函数,那就需要显示绑定,可以通过函数的call()和apply()方法来实现。

call(obj,arg1,arg1,...),apply(obj,[arg1,arg2,..]),他们的第一个参数是对象,是给this准备,在函数调用的时候将其绑定到this,通过这种方式你可以直接指向this绑定的对象,称之为显示绑定

function foo(){
  console.log(this.a)
}
var a = 'window'var obj ={
  a:'obj',
}
foo.call(obj)

显示绑定可以解决隐式丢失问题么?

function foo(){
  console.log(this.a)
}
var a = 'window'var obj ={
  a:'obj',
}
foo.call(obj) // 'obj'
setTimeout(foo,100)// 'window'

可以看出,并不能解决隐式丢失问题,虽然foo.call(obj) 将 this指向了obj,但是显示绑定实在调用的时候绑定this,函数调用完之后this已经不再受你控制,但是无法保确定函数作为参数传递给其他函数时,其他函数会怎样调用这个函数。

举个例子,你用到的一个工具函数,这个函数是异步,需要你在异步结束之后做些操作,传统的操作的是通过回调函数,等工具函数的异步操作完成之后,调用你传递的函数,但是你自己的函数中用到了this,并且期望this指向特定的对象,使用call 或者 apply来进行显示绑定,显示绑定是在函数调用时进行绑定的,这个时候你的函数已经执行了,而期望的是工具函数异步执行完之后再调用自己的函数,而工具函数内如何调用自己的函数,我们是不知道的,可以直接调用,可以显示绑定this,此时this的指向已经不受我们控制。

function myFun(){
  console.log(this.name)
}
var name = 'window'
var p1 = {
  name:'p1'
}
​
var p2 = {
  name:'p2'
}
myFun.call(p1); // 'p1'
// 工具函数
function util(fn){
  //函数调用 对于使用者是黑盒操作 不知道内部如何调用,因此this不受控
  fn()// 1、直接调用 'window'
  fn.call(p2) 显示绑定的调用// 'p2'
}
​
util(myFun)

我们知道了是因为函数作为参数 或者回调传递是,内部是如何调用的我们是不清楚的,那也就是this的绑定是不受我们控制的,要解决这个问题就是在函数被传递的时候 进行this绑定同时让函数不发生调用,这样无论工具函数怎样调用你的函数,this指向都是确定的

function myFun(){
  console.log(this.name)
}
var name = 'window'
var p1 = {
  name:'p1'
}
​
var p2 = {
  name:'p2'
}
​
// 工具函数
function util(fn){
  setTimeout(fn,100)
}
// 包裹函数
function bindFn(obj){
  return function(){
    myFun.call(obj)
  }
}
var fn = bindFn(p1) //返回的是函数,避免被直接调用
util(fn) // fn 执行的时候手动调用显示绑定,绑定this

创建一个函数包裹,在每次函数执行的时候将this绑定到预期的对象上,同时返回一个函数,避免函数被直接调用了,看到这里,有没有想起我们常用的bind函数?

bind源码:developer.mozilla.org/zh-CN/docs/…

if(!Function.prototype.mBind)(function(){
    var slice = Array.prototype.slice
    Function.prototype.mBind = function(){
      var self = this;
      var args = slice.call(arguments,1)
      var target = arguments[0]
      if(typeof self !=='function'){
        throw new TypeError('Function.prototype.bind - ' +
        'what is trying to be bound is not callable');
      }
      return function(){
        return self.apply(target,args.concat(slice.call(arguments)))
      }
    }
  })()
​
​
  var person = {
    name:'ddd',
    say:function(){
      console.log(this.name)
    }
  }
​
  var say = person.say;
  var b =say.mBind(person)
  b()

4.new绑定

使用new来调用函数,或者说当函数发生函数调用的时候,会自动执行下面的操作

(1)创建一个全新的对象

(2)这个新对象执行[[Prototype]]连接

(3)新对象绑定到函数调用的this

(4)如果函数没有返回其他对象,那么new 表达式中的函数会自动返回这个新对象

看一下new操作的结果

function Person(name,age){
  this.name = name ;
  this.age = age;
}
var  p = new Person('sss',122)
console.log(p)

截屏2021-08-08 下午3.30.40.png 自己实现:

function newOperator(ctor){
    // 首先判断
    if(typeof ctor !=='function'){
      throw 'newOperator function the first param must be a function';
    }
    // 1.创建一个新的对象
    var obj = {}
    // 2.创建[[prototype]]连接
    obj.__proto__ = ctor.prototype
    // 3. 绑定this到新对象
    var args = Array.prototype.slice.call(arguments, 1);
    var result = ctor.apply(obj,args)
    // 4. 判断返回值
    var isObject = typeof result === 'object' && result !== null;
    var isFunction = typeof result === 'function';
    return isObject || isFunction ? result : obj;
  }
​
  function Person(name,age){
    this.name = name ;
    this.age = age;
  }
  var p1 = newOperator(Person,"ddd",'123')
  console.log(p1)

截屏2021-08-08 下午3.34.11.png

1和2 步骤可以用es6的Object.create(ctor.prototype),创建一个新的对象并把进行[[prototype]]连接

优先级

this绑定有4种规则,那如果同时应用了多种,优先级又是怎样的呢?

默认绑定毋庸置疑一定优先级最低

隐式绑定和显示绑定
function say(){
  console.log(this.name)
}
var p1 ={
  name:'p1',
  say:say
}
var p2 = {
  name:'p2',
  say:say
}
p1.say.call(p2);// p2
p1.say.call(p1)// p1

由此看书显示绑定的优先级高于隐式绑定

new绑定和隐式绑定
function foo(name){
    this.name = name
  }
  var p1 = {
    foo:foo
  }
  var p2 ={}
​
  p1.foo('p1') 
  console.log(p1.name) //p1
​
  p1.foo.call(p2,'p2') 
  console.log(p2.name) //p2var p3 = new p1.foo('p3')
  console.log(p1.name) //p1
  console.log(p3.name) //p3

由此看书new绑定的优先级高于隐式绑定

总结

  1. 函数中是否存在new调用?如果是 this绑定的是新创建的对象

    var p = new Person()
    
  2. 函数通过 call、apply、bind调用?如果是this则指向绑定的对象

    foo.call(p)
    
  3. 函数是否在某个对象的上下文中调用(隐式调用)?如果是this则指向该上下文对象

    var obj = {
      name:'dd',
      foo:foo
    }
    obj.foo()
    
  4. 以上都不属于,则会默认绑定,严格模式this绑定到undefined,非严格模式则绑定到window

扩展

箭头函数

箭头函数不使用this的4条规则,而是根据外层函数或者全局作用域来决定this

call实现

Function.prototype.myCall = function(context){
    var context = context ? context : window
​
    context.fn = this
    var args = Array.prototype.slice(arguments,1)
    var result = context.fn(args)
​
    delete context.fn
    
    return result
  }
  

apply实现

  Function.prototype.myApply = function(context) {
    context = context ? Object(context) : window
      context.fn = this
      let args = [...arguments][1]
      if (!args) {
          return context.fn()
      }
      let result = context.fn(args)
      delete context.fn;
      return result
   }
  

练习

var obj = {
    a: 10,
    b: this.a + 10,  
    fn: function() {
      return this.a; 
    }
  }
  
  console.log(obj.b); //NaN this.a this指向window this.a = undefined
  console.log(obj.fn()); // 10 this指向obj 隐式绑定规则
var a = 20;
var obj = {
  a: 10,
  getA: function() {
    return this.a;
  }
}
console.log(obj.getA()); // 10  this指向obj 隐式绑定规则
var test = obj.getA; 
​
console.log(test());  // 20 赋值之后 test中this 指向全局 默认绑定
    var a = 5;
    // 顶层函数:this指向window
    function fn1() {
      var a = 6;
      console.log(a); 
      console.log(this.a); // this指向调用者
    }
    
    function fn2(fn) {  // 此处相当于隐式赋值 fn = f1
      var a = 7;
      fn();  //window调用
    } 
    
    var obj = {
      a: 8,
      getA: fn1
    }
    fn2(obj.getA); // 6 当前作用域中a=6; 5 this指向的是全局作用域window
function fn() {
        // "use strict";
        // 严格模式下:禁止this关键字指向全局对象
        console.log(this); //undefined
        var a = 1;
        var obj = {
          a: 10,
          c: this.a + 20  //不能为undefined添加属性
        }
        return obj.c;
 }
 
 console.log(fn()); //Cannot read property 'a' of undefined
function Person(name, age) {
  this.name = name;
  this.age = age;
  console.log(this);//使用new创建出来的对象,输出Person
}
​
Person.prototype.getName = function() {
  console.log(this); //使用哪个对象来调用,this就指代谁,输出Person
}
var p1 = new Person("test", 18)
​
p1.getName() 
​
var obj = {
  foo: "test",
  fn: function() { //对象的方法内,this指向该对象
  var mine = this;
  console.log(this.foo); //"test" 
  console.log(mine.foo); //"test" 
  
  // 方法的内部函数的this指向window
  (function(){
  console.log(this.foo); //undefined
  console.log(mine.foo); //"test"  mine指向方法的局部变量mine,最终指向obj对象
  })()
  }
}
obj.fn();
​
function foo() {
  console.log(this.a);
}
​
var a = 2;
var o = {
  a: 3,
  foo: foo
}
var p = {
  a: 4,
}
o.foo(); // 3 this指向o 
(p.foo = o.foo)();  2 this 自调用函数指向全局p.foo = o.foo;
p.foo();  4 // this 指向p
function foo() {
  console.log(this.a);
}
var obj1 = {
  a: 3,
  foo: foo
};
var obj2 = {
  a: 5,
  foo: foo
};
obj1.foo();//3 隐式绑定
obj2.foo();//5 隐式绑定
obj1.foo.call(obj2)//5 显示绑定
obj2.foo.call(obj1)//3 显示绑定