JavaScript this详解:指向绑定规则及其优先级,this的注意事项,this的绑定方法

99 阅读9分钟

this的概念:

this就是属性或方法当前所在的对象(重点)

  例子说明

let obj = {
  a:1,
  logContext: function () {
    console.log('obj:', this)
  }
}
obj.logContext()//打印obj对象

  上面代码中,this 其实就是 obj 对象,this在logContext函数中被调用,而 logContext 属于 obj 对象上的一个方法。

  如果我们对代码 {4} 进行改造,改造如下。

// ...
  logContext: function () {
    console.log('obj:', this, this.a)
  }
// ...

  在执行完改造代码后会发现打印 "1", 这也说明当前的 this 指向 obj

  我们再来看一个特殊的例子, 在全局作用域定义一个 logContext

function logContext () {
  console.log(this)
}

logContext();//window
//浏览器环境打印window,node环境打印global对象,严格环境下打印undefined

  分析:logContext 的 this 指向取决于当前所在的对象,由于目前 logContext 是定义在全局环境下,相当于是直接挂在 window 对象下的。

  我们在执行代码 {5} 的时候,其实相当于隐式调用(window.logContext),所以 logContext 所在的当前对象是 window

  由于对象的属性是可以赋值给另一个对象,所以属性当前的对象是可变的,即this的指向是可变的

  既然知道了一个函数中的 this 就是所属于当前所在的对象,那我们再看下面这个案例,来思考一下。

  我们再次定义了一个 animal 对象,并执行关键的第 {16} 行代码,那我们思考一下,执行第 {17} 行代码,

  此时的 this 指向谁?而 animal.logContext 的函数引用又指向谁,让我们分析一下

let obj = {
  a:1,
  logContext: function () {
    console.log('obj:', this)
  }
}
obj.logContext()//展示'obj',


let animal = {
  name: 'monkey',
  logContext: function () {
    console.log('animal', this);
  }
}
animal.logContext = obj.logContext
animal.logContext()
  1. 由于将 obj.logContext 指向了 animal.logContext, 所以 animal.logContext 的函数引用也指向了 obj.logContext, 也就是说在执行 第 {17} 行代码 的时候,虽然调用的是 animal.logContext,实际执行的是 obj.logContex 方法
  2. this 是指向函数当前所在的对象,那我们想一下, animal.logContext 的引用虽然改变,但是它所在的对象并没有发生改变,所以 this 仍然指向 animal

this指向绑定规则(默认/隐式/new/显示绑定)

1.默认绑定:独立的函数调用,可以理解为没有被绑定到某个对对象上进行调用(非严格模式下指向window,严格模式指向undefined)

let obj = {
    name:'why',
    bar: function() {
        console.log('bar: ',this);
    }
}
let baz = obj.bar;//将bar函数赋值给baz(引用赋值,赋值地址)
baz()//指向window对象(非严格模式)

2.隐式绑定:通过某个对象进行调用

隐式绑定前提条件:

1.在调用的对象内部有一个对于函数的引用(比如一个属性)

2.没有这样的引用,在进行调用的时候会报找不到该函数的错误

3.通过这个引用,将this绑定到了这个对象上面

function foo() {
    console.log('foo is handsome', this);
}
let obj = {
    name:'qqq'
}
obj.aaa =  foo;//将foo赋值给obj的aaa对象
obj.aaa()//this此刻绑定到obj对象

3.new绑定

使用new关键字来调用函数会执行以下的操作:

1.创建一个全新的对象

2.这个新对象会被执行prototype连接

3.这个新对象会被绑定到函数调用的this上(this绑定在这个步骤完成)

4.如果函数没有返回其他对象,表达式会返回这个新对象

  步骤 1: 创建一个全新的对象。假设这个对象为 obj

const obj = {}; // 这个操作在内部进行,不需要显式写出

  步骤 2: 这个新对象会被执行 prototype 连接。也就是说,新对象的 proto 属性会被设置为构造函数的 prototype 属性。

obj.proto = Person.prototype;

  步骤 3: 这个新对象会被绑定到函数调用的 this 上。

Person.call(obj, 'Alice', 30); // 将构造函数的 this 绑定到新对象上,并传入参数

  步骤 4: 如果构造函数没有显式返回其他对象,表达式会返回这个新对象。

const person1 = obj; // 如果构造函数没有显式返回对象,则返回新创建的对象

4.显示绑定(通过call,apply函数进行this绑定)


this四条规则优先级

1.默认规则的优先级最低

function defaultBinding() { console.log(this) }  
// 非严格模式下,this 绑定到全局对象
defaultBinding(); // 输出: 全局对象(window)
// 严格模式下,this 绑定为 undefined
'use strict'; 
defaultBinding(); // 输出: undefined

2.显示绑定优先级高于隐式绑定

const obj = {
  value: 42,
  method: function() {
    console.log(this.value);
  }
};

const anotherObj = { value: 100 };

// 隐式绑定:this 绑定到 obj
obj.method(); // 输出: 42

// 显示绑定:this 绑定到 anotherObj
obj.method.call(anotherObj); // 输出: 100

3.new绑定优先高于隐式绑定

function Person(name) {
  this.name = name;
  console.log(this);
}

const obj = {
  Person: Person
};

// 隐式绑定将 this 绑定到 obj
obj.Person('Alice'); // 输出: obj

// new 绑定将 this 绑定到新创建的对象
const newPerson = new obj.Person('Bob'); // 输出: 新创建的 Person 对象

4.new绑定优先级高于bind

function Person(name) {
  this.name = name;
  console.log(this);
}

const anotherObj = {};

// 使用 bind 显示绑定 this 到 anotherObj
const BoundPerson = Person.bind(anotherObj);

// new 绑定优先于 bind,this 绑定到新创建的对象
const newPerson = new BoundPerson('Charlie'); // 输出: 新创建的 Person 对象
console.log(newPerson.name); // 输出: Charlie
console.log(anotherObj.name); // 输出: undefined

a.new绑定可以与bind一起使用,new绑定优先级更高

function Person(name) {
  this.name = name;
  console.log(this);
}

const anotherObj = {};

// 使用 bind 创建一个绑定到 anotherObj 的函数
const BoundPerson = Person.bind(anotherObj);

// 使用 new 调用 BoundPerson
const newPerson = new BoundPerson('Charlie'); // 输出: 新创建的 Person 对象
console.log(newPerson.name); // 输出: Charlie
console.log(anotherObj.name); // 输出: undefined

b.new绑定和call,apply不允许同时使用,不存在谁的优先级更高

function Person(name) {
  this.name = name;
  console.log(this);
}

// 使用 new 调用 Person
const newPerson = new Person('Dave'); // 输出: 新创建的 Person 对象

// 试图同时使用 new 和 call,会导致语法错误
const invalidCall = new Person.call({}, 'Eve'); // TypeError: Person.call is not a constructor

改变this指向的特殊情况

  1. 直接取出内层地址调用

    var obj ={
      foo: function () {
        console.log(this);
      }
    };
    // 情况一
    (obj.foo = obj.foo)() // window
    // 情况二
    (false || obj.foo)() // window/
    / 情况三
    (1, obj.foo)() // window
    

      js引擎内部,obj与obj.foo储存在两个内存地址,obj.foo()是从地址一调用地址二,因此地址二的运行环境是地址一,this指向obj,上面三种情况都是直接取出地址二进行调用,此时的运行环境为window

  1. 如果this所在的方法不在对象的第一层,这时this只是指向当前一层的对象,而不会继承更上面的层。

    var a = {
      p: 'Hello',
      b: {
        m: function() {
          console.log(this.p);
        }
      }
    };
    
    a.b.m() // undefined
    

      a.b.m在对象的第二层,m此时是普通的函数,而不是对象的方法,对于普通函数,非严格模式下指向全局对象,严格模式指向undefined


### 使用this注意点:

避免多层this

默认情况下,this的指向是不确定的,所以不要再函数中包含多层this

var o = {
  f1: function () {
    console.log(this);
    var f2 = function () {
      console.log(this);
    }();
  }
}

o.f1()
// Object
// Window

若想要f2函数内的this指向外层即f1,可以在f2定义一个指向外层的this变量(常见做法)

var o = {
  f1: function() {
    console.log(this);
    var that = this;
    var f2 = function() {
      console.log(that);
    }();
  }
}

o.f1()
// Object
// Object

绑定this的方法(显示绑定)

为什么需要显示绑定?

this的动态切换虽然增添了js的灵活性,但由于this的指向是不确定的,需要显示的把this固定下来(通过js提供的call,apply,bind方法)

1.Function.prototype.call()

  1. call方法的作用:call可以指定函数内部this的指向,然后在所指定的作用域中,调用该函数
var obj = {};

var f = function () {
  return this;
};

f() === window // true,浏览器环境
f.call(obj) === obj // true

上面的代码,指定f内部的this指定对象obj,然后在obj的作用域运行函数f

  1. call方法的参数是一个对象,如果参数为空,null,undefined则默认传入全局对象
var n = 123;
var obj = { n: 456 };

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

a.call() // 123
a.call(null) // 123
a.call(undefined) // 123
a.call(window) // 123
a.call(obj) // 456

为什么var定义的变量挂载到全局环境中了?

var定义的变量具有函数作用域和全局作用域,即便在代码块中定义,var声明的变量依然会被提升到函数或全局作用域顶部;

let、const定义的变量具有块级作用域,仅在声明所在的代码块内有效,全局作用域中,let、const声明的变量不会称为全局对象的属性

  1. 如果call方法的参数是一个原始值,那么这个原始值会自动转成对应的包装对象
var f = function () {
  return this;
};

f.call(5)
// Number {[[PrimitiveValue]]: 5}

  上面代码中call的参数为5,不是对象,会被自动转成包装对象:Number的实例来绑定f内部的this

  1. call方法还可以接受多个参数
func.call(thisValue, arg1, arg2, ...)

  call的第一个参数就是this所要指向的那个对象,后面的参数是函数调用时所需的参数

  1. call方法的一个应用是调用对象的原生方法
var obj = {};
obj.hasOwnProperty('toString') // false
// 覆盖掉继承的 hasOwnProperty 方法
obj.hasOwnProperty = function () {
  return true;
};
obj.hasOwnProperty('toString') // true
Object.prototype.hasOwnProperty.call(obj, 'toString') // false

  上面代码中,hasOwnProperty被覆盖,就无法使用obj.hasOwnProperty这种方式得到原生方法调用的结果,但是通过call,使用Object.prototype.hasOwnProperty.call(obj, 'toString'),将hasOwnProperty中的this绑定到obj上面,将在obj作用域内使用hasOwnProperty函数,这样无论obj上是否有同名方法,都不会影响结果

2.Function.prototype.apply()

  1. apply作用:作用与call方法类似,也是改变this指向然后调用函数,唯一的区别是接受数组作为函数执行时的参数
func.apply(thisValue, [arg1, arg2, ...])//使用格式
//使用案例
function f(x, y){
  console.log(x + y);
}

f.call(null, 1, 1) // 2
f.apply(null, [1, 1]) // 2

3.Function.prototype.bind()

  1. bind作用:将函数体内的this绑定到某个对象,然后返回一个新函数

### 箭头函数

1.箭头函数不会绑定this,arguments属性

2.箭头函数不能作为构造函数来使用(不能与new一起来使用,会抛出错误)

箭头函数的优化

1.箭头函数只有一个参数,()可以省略

2.函数体中只有一行执行代码,{}可以省略

3.只有一行代码时,这行代码的表达式结果会作为函数的返回值默认返回

4.如果默认返回值是一个对象,这个对象必须加上()

大括号 {} 在 JavaScript 中有两种用途:一种是用来表示代码块,另一种是用来表示对象字面量。没有小括号,JavaScript 解析器会将 {} 解释为代码块的开始。

const getDefaultObject = () => {   key: 'value' }; // 这是错误的
const getDefaultObject = () => ({   key: 'value' });//正确写法

自我提问

1.箭头函数不会绑定this,那么this从哪里来?

箭头函数使用的是词法作用域,它们从定义它们的上下文中“继承”了 thisarguments

2.为什么箭头函数不能作为构造函数来使用?(不能与new一起来使用,会抛出错误)

  a.箭头函数没有 [[Construct]] 方法,因此不能用 new 调用。

  b.箭头函数没有 prototype 属性,构造函数必须有一个 prototype 属性,这个属性会被新创建的对象实例所引用。