This
Published on Feb 10, 2023, with 13 view(s) and 0 comment(s)
Ai 摘要:本文详细解析了JavaScript中`this`的用法与指向规则。`this`的作用在于简化对象方法的引用,避免硬编码对象名。其指向取决于调用方式,包括默认绑定(独立调用)、隐式绑定(对象方法调用)、显示绑定(call/apply/bind)和new绑定(构造函数调用),优先级依次递增。箭头函数不绑定`this`,而是继承外层作用域的`this`值。文章通过代码示例对比了不同场景下`this`的指向,并分析了常见面试题,帮助开发者深入理解`this`的动态绑定特性。

this的作用

在面向对象编程中,this(或类似的关键字如self)用于引用当前对象实例。JavaScript虽然支持面向对象编程,但它的this机制更加灵活

var obj = {
  name: "sun",
  eating: function () {
    console.log(obj.name, "正在吃东西");
  },
  running: function () {
    console.log(obj.name, "正在吃跑步");
  },
  studying: function () {
    console.log(obj.name, "正在吃学习");
  },
};
obj.eating();
obj.running();
obj.studying();

假如说这里的obj对象名如果改成obj1,那么该对象里面的方法中的obj都需要改成,obj1,如果用this,就可以避免这一个繁杂的操作。 没有this也有解决方案,有this在编写代码时更方面。

this的指向

this在浏览器全局作用域下指向window,在node中执行一个空对象{},因为node在执行代码时会将js打包成一个模块进行加载、编译,放到一个函数中,再执行这个函数的call({})方法。 通常情况下都是出现在函数中。

function foo() {
  console.log(this);
}
// 调用方式一
foo(); //window

// 调用方式二
var obj = {
  foo: foo,
};
obj.foo(); //obj对象

// 调用方式三
foo.call("abc"); //String{"abc"}对象

上述案例中函数的调用方式不同产生了三种不同的打印结果,可见函数在调用时,JavaScript会默认给this绑定一个值,this的绑定和定义的位置没有关系跟this的绑定和调用方式以及调用的位置有关。this是在运行时被绑定的。

this的绑定规则

默认绑定:独立函数调用,可以理解成有没有被绑定到某个对象上进行调用

function foo() {
  console.log(this);
}
foo();//window
var obj = {
  foo: function () {
    console.log(this);
  },
};
var bar = obj.foo;
bar(); //window
function foo(func) {
  func();
}

var obj = {
  bar: function () {
    console.log(this);
  },
};

foo(obj.bar); //window

隐式绑定:通过某个对象发起的调用,在调用的对象内部会有一个对函数的引用(比如一个属性)

var obj = {
  eating: function () {
    console.log(this);
  },
};
obj.eating();//obj

显示绑定:在隐式绑定时,必须在调用函数的对象中有一个对函数的引用,比如一个属性,就是这个属性将函数的this绑定到该对象上。如果不希望有这个引用,又想对函数有一个引用,那么可以通过call、apply和bind方法。因为这些方法显示地指定了this地指向,所以被称为显示绑定。

javascript中的apply、call和bind

function foo(num1, num2) {
  console.log(num1 + num2, this);
}
var obj = {};

foo.call(window, 1, 2); //3 Window
foo.call(obj, 1, 2); //3 {}
foo.call(123, 1, 2); //3 Number {123}

foo.apply(obj, [1, 2]); //3 {}

var obj1 = foo.bind(obj, 1, 2);
obj1();//3 {}
// 实现一下call:用来将函数绑定到传入得对象上;
Function.prototype.sjcall = function (thisArg, ...args) {
  // 1,首先得执行该方法
  var fn = this;
  // 2,获取传入得对象,将函数绑定到该对象:即使用该对象隐式调用方法
  var obj = thisArg ? Object(thisArg) : window;
  obj.fn = fn;
  // 3,如果有参数
  var result = obj.fn(args);
  // 4,删去fn属性
  delete obj.fn;
  // 5,返回res
  return result;
};

new绑定:JavaScript中的函数可以当做一个类的构造函数来使用,也就是使用new关键字。 使用new关键字来调用函数是,会执行如下的操作:

  • 1.创建一个全新的对象;

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

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

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

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

  this.name = name;
}

var p = new Person("sun");
console.log(p); // Person {name: "sun"}

优先级: 

  1. new绑定 > 显式绑定 > 隐式绑定 > 默认绑定

  2. bind的优先级低于new

  3. nullundefined作为this绑定会被忽略,转为默认绑定

一些函数的this分析

//setTimeout
setTimeout(function () {
  console.log(this); //window
}, 1000);

//onclick
var box = document.querySelector(".box");
box.onclick = function () {
  console.log(this); //<div class="box"></div>
};

//forEach
var name = ["abc", "cba", "nba"];
var obj = { name: "sun" };
name.forEach(function () {
  console.log(this); // 三遍 {name: "sun"}
}, obj);

箭头函数

箭头函数是es6之后增加的一种编写函数的方法,并且它比函数表达式要更加简洁。箭头函数不会绑定this、arguments属性,也不能作为构造函数来使用。

nums.forEach((item,index,arr) => {})

箭头函数编写优化

1,如果有一个参数()可以省略。

nums.forEach(item=>{})

2,如果函数执行体只有一行代码,那么可以省略大括号,并且这行代码的返回值会作为整个函数的返回值

nums.forEach(item => console.log(item))
nums.filter(item => true)

3,如果函数执行体只有返回一个对象,那么需要给这个对象加上()

var foo = () => {
  return { name: "abc"}
}
var bar = () => ({name: "abc"})

箭头函数中的this箭头函数不适应this的四种标准规则,不绑定this,而是根据外层作用域来决定this。 这里使用settimeout来模拟网络请求,然后拿到obj对象,设置data;

不用箭头函数的情况: 拿到的this是window,需要在外层定义:this = this,在setTimeOut的回调函数中使用this就代表了obj对象。

var obj = {
  data: [],
  getData: function () {
    var _this = this;
    setTimeout(function () {
      var res = ["abc", "cba", "nba"];
      _this.data.push(...res);
    }, 1000);
  },
};

使用箭头函数: 箭头函数不绑定this对象,this引用就会从上层作用域中找到对应的this

var obj = {
  data: [],
  getData: function () {
    setTimeout(() => {
      var res = ["abc", "cba", "nba"];
      this.data.push(...res)
    }, 1000);
  },
};

如果getData也是一个箭头函数,那么settimeout中的回调函数中this指向谁呢? window

var obj = {
  data: [],
  getData: () => {
    setTimeout(() => {
      var res = ["abc", "cba", "nba"];
      this.data.push(...res);
    }, 1000);
  },
};

相关面试题

var name = "window";
var person = {
  name: "person",
  sayName: function () {
    console.log(this.name);
  },
};

function sayName() {
  var sss = person.sayName;
  sss(); //window 独立函数调用
  person.sayName(); //person 隐式调用
  (person.sayName)();//person js引擎解析后没有第一对(),相当于隐式函数调用
  (b = person.sayName)();//window 独立函数调用
}
sayName();
var name = "window";

var person1 = {
  name: "person1",
  foo1: function () {
    console.log(this.name);
  },
  foo2: () => console.log(this.name),
  foo3: function () {
    return function () {
      console.log(this.name);
    };
  },
  foo4: function () {
    return () => {
      console.log(this.name);
    };
  },
};
var person2 = {
  name: "person2",
};
person1.foo1(); //person1
person1.foo1.call(person2); //person2

person1.foo2(); //window
person1.foo2.call(person2); //window

person1.foo3()(); //window,独立函数调用。有this不需要向上层找
person1.foo3.call(person2)(); //window 独立函数调用
person1.foo3().call(person2); //person2

person1.foo4()(); //person1。没有this,向上层找
person1.foo4.call(person2)(); //person2。call(person2)将上层作用域绑定给了person2,
person1.foo4().call(person2); //person1
var name = "window ";
function Person(name) {
  this.name = name;
  this.foo1 = function () {
    console.log(this.name);
  };
  this.foo2 = () => console.log(this.name);
  this.foo3 = function () {
    return function () {
      console.log(this.name);
    };
  };
  this.foo4 = function () {
    return () => {
      console.log(this.name);
    };
  };
}

var person1 = new Person("person1");
var person2 = new Person("person2");
person1.foo1(); //person1
person1.foo1.call(person2); // person2 显示和隐式同时存在,显示作用

person1.foo2(); //person1  函数具有作用域
person1.foo2.call(person2); // person1

person1.foo3()(); //- window
person1.foo3.call(person2)(); //window
person1.foo3().call(person2); // person2

person1.foo4()(); // person1
person1.foo4.call(person2)(); // person2
person1.foo4().call(person2); // person1
var name = "window";
function Person(name) {
  this.name = name;
  this.obj = {
    name: "obj",
    foo1: function () {
      return function () {
        console.log(this.name);
      };
    },
    foo2: function () {
      return () => {
        console.log(this.name);
      };
    },
  };
}

var person1 = new Person("person1");
var person2 = new Person("person2 ");
person1.obj.foo1()(); //window
person1.obj.foo1.call(person2)(); //window
person1.obj.foo1().call(person2); // person2

person1.obj.foo2()(); // obj
person1.obj.foo2.call(person2)(); // person2
person1.obj.foo2().call(person2); //obj