this 详解
this 详解
Section titled “this 详解”在没有特别指明的情况下,下文的例子都是在浏览器环境。
关于严格模式和非严格模式的区别,可以参考:
严格模式和非严格模式:
console.log("this", this); // --> window函数上下文中的
this都是在函数调用的那一刻才知道的,也就是运行时决定的。
通常指的是独立函数调用,没有具体的调用者,典型的形式为
fun()。
非严格模式:
非严格模式中默认绑定中函数的this --> window。
var name = "xzq";function printThis() {  console.log("printThis", this); // window  console.log("printThis-name", this.name); // 'xzq'}printThis();严格模式:
严格模式中默认绑定中函数的this --> undefined。
var name = "xzq";function printThis() {  "use strict";  console.log("printThis", this); // undefined  console.log("printThis-name", this.name); // Uncaught TypeError: Cannot read properties of undefined (reading 'name')}printThis();扩展知识:
参考:https://es6.ruanyifeng.com/#docs/let#%E9%A1%B6%E5%B1%82%E5%AF%B9%E8%B1%A1%E7%9A%84%E5%B1%9E%E6%80%A7
var 在全局上下文中声明的变量会挂载到 window 上。
let, const 不会挂载到 window 上。
var name = 'xzq'。
let name = 'xzq'。
这里有一点需要注意,当你在 window 上声明的属性之后,你需要把浏览器当前网页关掉,重新打开,这个对应的属性才会消除。
setTimeout(cb, xx), setInterval(cb, xx)...这种类型的cb的调用方式也看做没有明确的调用者,那么其中cb的this的指向也可以应用默认绑定的规则。
函数是由某个对象上调用的,例如:
xxx.foo()。
非严格模式和严格模式:
this指向调用者,和函数自身所处的位置无关。
function printThis() {  console.log("printThis-name", this.name); // 'xzq'}const person = {  name: "xzq",  printThis,  subPerson: {    name: "subXzq",    printThis,  },};
person.printThis(); // 'xzq'person.subPerson.printThis(); // 'subXzq'注意:
如果将对象中的函数赋值出来,然后直接调用,那么就等同于第一种默认调用模式了。
const otherPrintThis = person.printThis;otherPrintThis(); // this --> 严格模式: undefined, 非严格模式: window指的是通过
call, apply, bind的第一个参数进行显式指定函数的this。例如:
foo.call(thisArg, ...args), foo.apply(thisArg, [args]), foo.bind(thisArg, ...args)()。
关于这三个方法具体的用法可以参考:
其中 call 和 apply 类似,只有一个区别,就是 call 方法接受的是一个参数列表,而 apply 方法接受的是一个包含多个参数的数组。
function foo() {}foo.call(thisArg, '1', '2', '3'...)foo.apply(thisArg, ['1', '2', '3'...])其中 call 和 bind 的参数列表是一样的,但是 call 会调用函数;而 bind 不会调用函数,而是返回绑定this之后的含函数。
function foo() {}foo.call(thisArg, '1', '2', '3'...)const otherFoo = foo.bind(thisArg, '1', '2', '3')otherFoo()关于这三者的第一个参数thisArg需要做出一些特殊说明:
严格模式下:
传入什么,这个函数的this就指向什么。
function printThis() {  "use strict";  console.log("printThis", this);}printThis.call({}); // {}printThis.call(1); // 1printThis.call(null); // nullprintThis.call(undefined); // undefined非严格模式下:
传入引用类型的值,那么这个函数的this就指向这个引用类型的值。
传入原始类型的值,那么这个函数的this就指向这个原始类型的值对应的包装对象。
传入null, undefined,那么这个函数的this就指向window。
function printThis() {  console.log("printThis", this);}printThis.call({}); // {}printThis.call(1); // Number { 1 } --> new Number(1)printThis.call(null); // windowprintThis.call(undefined); // window综合示例(非严格模式下):
var name = "xzq";function printThis() {  console.log("printThis-name", this.name);}const person = {  name: "xzq",  printThis,  subPerson: {    name: "subXzq",  },};
person.printThis.call(person.subPerson); // 'subXzq'printThis.apply(person); // 'xzq'printThis.call(null); // 'xzq', 此时 this --> windowprintThis.apply(undefined); // 'xzq', 此时 this --> windownew 绑定
Section titled “new 绑定”指的是通过
new调用一个函数,此时的this指向一个新的对象。例如:
new foo()。
参考:new(MDN)
通过new调用函数时,JS引擎内部会对该函数有隐式的如下4步操作:
- 创建一个空的 JS对象(即{});
- 为步骤1新创建的对象添加属性__proto__,将该属性链接至构造函数的原型对象;
- 将步骤 1新创建的对象作为该函数的this,也就是改变该函数的this指向,这里可以通过显式绑定的方式,例如:call, apply, bind;
- 如果该函数没有返回对象(包含 Functoin, Array, Date, RegExg, Error...),则返回this;
按照这个规则,我们自己实现以下new运算符:
function callFunctionByNew(func, ...args) {  // 步骤 1  const tmpObj = {};
  // 步骤 2  tmpObj.__proto__ = func.prototype;
  // 步骤 3  const res = func.call(tmpObj, ...args);  // 如果构造函数中有手动返回一个对象(Functoin, Array, Date, RegExg, Error...)  if (res !== null && (typeof res === "object" || typeof res === "function"))    return res;
  // 步骤 4  return tmpObj;}调用new func()之后的返回值有两种情况:
- func中没有显式返回一个引用类型的值(包含- Functoin, Array, Date, RegExg, Error...),那么直接返回一个实例化后的对象;
- func中显式返回一个引用类型的值(包含- Functoin, Array, Date, RegExg, Error...),那么直接返回该值;
但是this的指向却和new func()的返回值无关,new调用函数中this的指向是指向一个新创建的对象,也就是我们上例中传入的tmpObj。
() => {},这是ES6的一种定义函数的方式,该函数没有自己的this,它的this是由它的上级作用域中的this决定。
关于箭头函数具体可以参考:
箭头函数需要注意的几点:
- 箭头函数没有自己的 this对象,箭头函数的this取决于它的上级作用域中的this;
- 由于箭头函数没有自己的 this指针,调用显式绑定方式调用该函数时(call, apply, bind),它们的第一个参数会被忽略,而只能正常识别传递后面的参数列表,也就是说无法绑定函数的this,这也就意味着箭头函数的this是无法通过显式绑定方式改变的;
- 不可以当作构造函数,也就是说,不可以对箭头函数使用 new运算符,否则会抛出一个错误;
- 没有自己的 arguments对象,当然,如果父级作用域有arguments,它是可以正常和访问变量一样使用的。如果要用,可以用rest参数代替;
- 不可以使用 yield命令,因此箭头函数不能用作Generator函数;
举一个例子:
var name = "window";
var person1 = {  name: "person1",  show1: () => console.log(this.name),  show2: function () {    return () => console.log(this.name);  },};var person2 = { name: "person2" };
person1.show1(); // windowperson1.show1.call(person2); // 箭头函数 call 的第一个参数会被忽略, window
person1.show2()(); // 对象调用 + 箭头函数 person1person1.show2().call(person2); // 对象调用 + 箭头函数 call 的第一个参数会被忽略, person1person1.show2.call(person2)(); // 对象调用 + call + 箭头函数 person2参考:let 具有块级作用域
在ES6之前只有两种作用域,分别是全局作用域和函数作用域,在ES6时多出来一种块级作用域。
我们这里需要知道的就是前两种就够了。
person.show1():箭头函数是没有自己的this的,它依赖于的上级作用域中的this,这里它的上级作用域是全局作用域,也就是说不管是严格模式还是非严格模式,它的this都是指向window。
person1.show1.call(person2):箭头函数通过显式绑定的方式(call, apply, bind)指定this时,指定是无效的,也就是说这里的箭头函数的this依旧是寻找上级作用域中的this,同样指向window。
person1.show2()():
- const func = person1.show2()得到一个箭头函数;
- func()
这个func就是箭头函数,它的this和它上级作用域的this绑定的,这里我们发现它的上级作用域是函数作用域,也就是show2这个函数的作用域,show2函数的是由person1对象调用的,也就是隐式绑定的方式指定的this,那么show2函数的this指向person1,因此func的this指向person1。
person1.show2().call(person2):
- const func = person1.show2()得到一个箭头函数;
- func.call(person2)
这个func就是箭头函数,箭头函数通过显式绑定的方式(call, apply, bind)指定this时,指定是无效的,也就是说这里的箭头函数的this依旧是寻找上级作用域中的this,同样指向show2函数中的this,也就是person1。
person1.show2.call(person2)():
- const func = person1.show2.call(person2)得到一个箭头函数;
- func()
这个func就是箭头函数,它的this和它上级作用域的this绑定的,这里我们发现它的上级作用域是函数作用域,也就是show2这个函数的作用域,show2函数的是由call(person2)的方式调用,也就是显式绑定的方式指定的this,那么show2函数的this指向call的第一个参数person2,因此func的this指向person2。
注意:
有很多人箭头函数中的this是固定的,在定义时就决定了的。而普通函数的this是在运行时才知道的,但是经过上面我们的例子发现,其实箭头函数的this不是固定的。
因为箭头函数的this和它的上级作用域中的this是绑定的,如果它的上级作用域是普通函数中的函数作用域,那么该箭头函数中的this也是在该普通函数调用时才决定。
DOM 事件绑定函数
Section titled “DOM 事件绑定函数”通过
addEventerListener(eventName, cb)或者onEventName --> onclick = cb的方式给DOM元素注册的回调函数中的this指向绑定该事件的元素,也就是和ev.target相同。
<head>  <meta charset="UTF-8">  <meta http-equiv="X-UA-Compatible" content="IE=edge">  <meta name="viewport" content="width=device-width, initial-scale=1.0">  <title>Document</title></head>
<body>  <ul>    <li>1</li>    <li>2</li>    <li>3</li>  </ul>
  <script>    const ul = document.querySelector('ul')    ul.addEventListener('click', function (ev) {      console.log('ev-currentTarget', ev.currentTarget) // 不会变化,指向当前绑定事件的元素      console.log('ul-this', this) // 不会变化,同样指向当前绑定事件的元素      console.log('ev.currentTarget === this', ev.currentTarget === this) // true
      console.log('ev-target', ev.target) // 会变化,指向触发事件处理函数的元素    })  </script></body>
</html>关于currentTarget, target可以参考:
currentTarget 指向当前绑定事件的元素,它是不可变的,而 target 指向触发当前事件的元素,因此可变。
判断this的指向可以总结为如下几步:
- 当前this在全局上下文还是在函数上下文,如果是全局上下文,那么直接就是window,如果是函数上下文就接着往下走;
- 对于函数上下文,我们可以看函数的调用方式被划分为哪一种类别:
- 如果是foo()的形式,或者setTimeout(foo, timeout), setInterval(foo, timeout),那么采用默认绑定的规则;
- 如果是xxx.foo()的形式,那么采用隐式绑定的规则;
- 如果是foo.call(thisArg, ...args), foo.apply(thisArg, [args]), foo.bind(thisArg, ...args)()的形式,那么采用显式绑定的规则;
- 如果是new foo()的形式,那么采用new绑定的规则;
- 如果该函数是() => xxx,那么采用箭头函数的规则;
- 如果当前函数是通过 addEventerListener(eventName, foo)或者onEventName --> onclick = foo的方式注册的,那么采用DOM事件绑定函数的规则;
 
- 如果是
var name = "window";
var person1 = {  name: "person1",  show1: function () {    console.log(this.name);  },  show2: () => console.log(this.name),  show3: function () {    return function () {      console.log(this.name);    };  },  show4: function () {    return () => console.log(this.name);  },};var person2 = { name: "person2" };
person1.show1(); // 对象调用 person1person1.show1.call(person2); // call person2
person1.show2(); // 箭头函数 windowperson1.show2.call(person2); // 箭头函数 call 的第一个参数会被忽略, window
person1.show3()(); // 普通调用 windowperson1.show3().call(person2); // call person2person1.show3.call(person2)(); // 普通调用 window
person1.show4()(); // 对象调用 + 箭头函数 person1person1.show4().call(person2); // 对象调用 + 箭头函数 call 的第一个参数会被忽略,person1person1.show4.call(person2)(); // 对象调用 + call + 箭头函数 person2var name = "window";
function Person(name) {  this.name = name;  this.show1 = function () {    console.log(this.name);  };  this.show2 = () => console.log(this.name);  this.show3 = function () {    return function () {      console.log(this.name);    };  };  this.show4 = function () {    return () => console.log(this.name);  };}
var personA = new Person("personA");var personB = new Person("personB");
personA.show1(); // new + 对象调用,personApersonA.show1.call(personB); // new + 对象调用 + call,personB
personA.show2(); // new + 对象调用,personApersonA.show2.call(personB); // new + 箭头函数 call 的第一个参数会被忽略, personA
personA.show3()(); // new + 对象调用 + 普通调用,windowpersonA.show3().call(personB); // new + 对象调用 + call,personBpersonA.show3.call(personB)(); // new + 对象调用 + call + 普通调用,window
personA.show4()(); // new + 对象调用 + 箭头函数,personApersonA.show4().call(personB); // new + 对象调用 + 箭头函数 call 的第一个参数会被忽略,personApersonA.show4.call(personB)(); // new + 对象调用 + call + 箭头函数, personB测试题 3(最难)
Section titled “测试题 3(最难)”知识点:
- 原型链
- 变量提升
this- 运算符优先级
// 测试题function Foo() {  // 这个会污染全局的 getName  getName = function () {    console.log(1);  };  return this;}Foo.getName = function () {  console.log(2);};Foo.prototype.getName = function () {  console.log(3);};var getName = function () {  console.log(4);};// 函数声明是预编译阶段就已经放到 AO 中了,还没到执行阶段function getName() {  console.log(5);}
//请写出以下输出结果:Foo.getName(); // 2getName(); // 4
// 执行 Foo() 之后,全局的 getName 被覆盖了Foo().getName(); // 此时函数内部的 this --> window,那么此时的 getName() 对应的应该是外界的 getName,结果为 1
getName(); // 1
// 运算符的优先级,new 带不带参数优先级是不同的  带参数优先级比不带参数优先级更高// 带参数指的是 new 后面的表达式中有()// 不带参数就是后面的表达式没有()
// new 带参数new Foo.getName(); // --> (new (Foo.getName)()) 这里要注意的是 getName 后面的括号不能看作函数调用,而要看作 new ... (...) 带参数// 1. getName = Foo.getName// 2. new getName() , 将 getName 用 new 来调用 这里会输出 2,然后返回值是 {}
// new 带参数new Foo().getName(); // --> ((new Foo()).getName)() 这里要注意的是 getName 后面的括号不能看作函数调用,而要看作 new ... (...) 带参数// 1. foo = new Foo()// 2. getName = foo.getName --> Foo.prototype.getName ... 原型链查找// 3. getName() --> 输出 3
new new Foo().getName(); // --> (new ((new Foo()).getName)()) 这里要注意的是 getName 后面的括号不能看作函数调用,而要看作 new ... (...) 带参数// 1. foo = new Foo()// 2. getName = foo.getName --> Foo.prototype.getName ... 原型链查找// 3. new getName() 将 getName 用 new 来调用 这里会输出 3,然后返回值是 {}