this 详解
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 --> window
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 事件绑定函数
通过
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
事件绑定函数的规则;
- 如果是
经典面试题
测试题 1
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 + 箭头函数 person2
测试题 2
var 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(最难)
知识点:
- 原型链
- 变量提升
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,然后返回值是 {}