JS基础 - 判断 this 的指向
this 是继承自父级的执行上下文,且 JS 中 this 的指向在函数定义的时候是确定不了的,只有函数执行的时候才能确定 this 到底指向谁(箭头函数除外),实际上 this 的最终指向的是那个调用它的对象
直接调用
直接调用是指通过函数名()
的方式调用。
function f(){
let a = 'A';
console.log(this.a); // undefined
console.log(this); // Window
}
f();
这里的 fn()
实际上是在全局对象上调用,即 Window.fn()
,所以 this 指向的是 Window
。
将例子复杂化:
const f = {
name : " A ",
showThis: function(){
console.log(this); // Object f
function bar(){ console.log(this) }; // Window
bar(); // 此处相当于 Window.bar()
}
}
f.showThis();
方法调用
方法调用是指通过对象来调用其方法函数。
const o = {
a: 'A',
b: function(){
console.log(this.a);
}
}
o.b(); // A
这里是 o
在调用 b()
,所以 this 指向的是 o
,所以就取到了 A 。
如果是多层嵌套会是什么样呢?
结论:不论嵌套了多少层,都指向最近的调用函数的对象,看个例子:
const o = {
a: 'A',
b: {
a: 'A2',
c:function(){
console.log(this.a);
}
}
}
o.b.c(); // A2
最近的调用函数的对象内没有需要的变量 a,那么会会逐级向上查找吗?
结论:会返回 undefined,this 的指向只会指向他的上一级对象,即使上上一级对象内有需要的东西。
将上面的例子改一下,删除 b 内部的 a:
const o = {
a: 'A',
b: {
c:function(){
console.log(this.a);
}
}
}
o.b.c(); // undefined
此外还有一种特殊情况,是直接调用和方法调用的混合:
const o = {
a: 'A',
b: {
a: 'A2',
c:function(){
console.log(this.a);
}
}
}
const f = o.b.c;
f(); // undefined
看起来是方法调用,但最后调用 fn()
时是直接调用,Window
中没有定义 a
,所以返回 undefined。
立即执行函数
立即执行函数,立即执行函数不管是在全局定义还是在函数内定义,this 都指向 Window。
Window.a = 5;
(function(){ console.log(this.a) })(); // 5,此时 this === Window
箭头函数
箭头函数并不会创建其自身的执行上下文,所以箭头函数中的 this 取决于它的外部函数。且这个指向在定义的时候就已经确定了,并不会在调用时指向其执行环境的对象,所以 call、apply 和 bind 方法并不能对箭头函数起作用。
var x=11;
let obj={
x:22,
say:()=>{
console.log(this.x);
}
}
obj.say(); // 11
上述代码中,箭头函数里面的 this 向外寻找外层函数,但 obj 不是个函数也没有外层函数包裹,所以 this 最终指向了 Window。
需要注意的是:如果这里的 var 替换成 let,则打印出的是 undefined,因为 var 和 function 声明的全局变量是顶级对象 window 的属性,而 let / const 声明的全局变量不属于顶级对象的属性了。
如果将上面代码改造一下,在外层包裹一个自执行函数,此时 this 就指向了自执行函数:
var x=11;
(function(){
this.x = 33;
let obj={
x:22,
say:()=>{
console.log(this.x);
}
}
obj.say(); // 33
})()
构造函数
构造函数需要使用 new 关键字构建实例,通常会经历以下 4 个步骤:
- 创建一个对象:let newObj = {};
- 将构造函数的原型赋值给新对象:Object.setPrototypeOf( newObject , foo.prototype);
- 更改构造函数 this 指向新对象,然后执行构造函数的代码:foo.call( newObj );
- 返回新对象;
所以使用 new 运算符构建实例时,this 会指向新生成的对象:
function f(){
this.a = 'A';
}
const b = new f();
console.log( b.a ) // A
在构造函数中,return 会影响 this 的指向:当 return 回来的值是对象时,那么 this 就会指向这个返回的对象,如果不是对象,则会指向原函数实例。
function f(){
this.a = 'A';
return {}
}
const b = new f();
console.log( b.a ) // undefined
function f(){
this.a = 'A';
return 1
}
const b = new f();
console.log( b.a ) // A
再来看一下类内部函数在外部调用时的场景:
class Demo {
constructor(x, y) {
this.x = x;
this.y = y;
}
sum() {
let sumVal = this.x + this.y;
return sumVal;
}
}
let myDemo = new Demo(2, 3);
const sum = myDemo.sum; // 将sum的引用赋值给新变量sum
sum();// Uncaught TypeError: Cannot read properties of undefined (reading 'x')
此时的 this 还是会按照方法调用的规则,指向运行时所在的环境(Window),所以找不到对应的 x 值。
那么,如何实现 this 指向实例呢?
可以在类的构造函数中,显式绑定 this:
class Demo {
constructor(x, y) {
this.x = x;
this.y = y;
this.sum = this.sum.bind(this); // sum中this显式绑定
}
sum() {
let sumVal = this.x + this.y;
return sumVal;
}
}
let myDemo = new Demo(2, 3);
const sum = myDemo.sum;
sum(); // 5
或者使用箭头函数来申明函数:
class Demo {
constructor(x, y) {
this.x = x;
this.y = y;
}
sum = () => {
let sumVal = this.x + this.y;
return sumVal;
}
}
let myDemo = new Demo(2, 3);
const sum = myDemo.sum;
sum(); // 5
修改 this 指向
改变 this 指向主要有三种方法:bind、call、apply ,其中 bind 对 this 指向所造成的影响最为深远,慎用。
bind 将当前函数和对象绑定,并返回一个新的函数(注意是返回一个新函数而不是修改原函数的 this 指向),不论新函数以何种方式调用,this 始终指向绑定的对象。
const o = {};
function a(){
console.log(this === o);
}
const b = a.bind(o);
a(); // false
b(); // true
使用了 bind 后,使用 apply 或者 call 都无法再改变 this 的指向。
const o = {};
function a(){
console.log(this === o);
}
const b = a.bind(o);
b.apply({})
b(); // true
call 和 apply 的区别:传参的方式不同,call 接收若干个参数列表,而 apply 接收一个由多个参数组成的数组
func.call( thisArg , arg1 , arg2 ,… )
const foo = function (sex,address) {
console.log(this.name);
console.log(sex);
console.log(address);
}
const Lucy = {
name: 'lucy',
}
foo.call(Lucy, 'male', 'hangzhou'); // Lucy,male,hangzhou
func.apply( thisArg , [ arg1 , arg2 ,… ] )
const foo = function (sex,address) {
console.log(this.name);
console.log(sex);
console.log(address);
}
const Lucy = {
name: 'lucy',
}
foo.apply(Lucy, ['male', 'hangzhou']); // Lucy,male,hangzhou
需要注意的一点是:在非严格模式下,如果传入的第一个参数为 undefined 或 null 或不传,那么 this 会默认指向 Window 对象,而在严格模式下,this 严格指向传入的第一个参数。
手写一个 call 方法
承接上一个例子
const foo = function (sex,address) {
console.log(this.name);
console.log(sex);
console.log(address);
}
const Lucy = {
name: 'lucy',
}
考虑使用 call 方法,foo 中的 this 指向调用他的对象,所以如果 foo 要获取到 this.name,就需要实现Lucy.foo()
这样的调用方式,那么 foo 应该变为 Lucy 的一个属性,如下所示:
const Lucy = {
name: 'lucy',
foo:function (sex,address) {
console.log(this.name);
console.log(sex);
console.log(address);
}
}
所以 call 方法的本质就是,给 Lucy 增加一个临时属性,并在调用后删除该属性。
首先在 Function 原型链上定义一个 call2 的属性:
Function.prototype.call2 = function(target,...args){}
这里不能使用箭头函数,如果是箭头函数,那么函数中的 this 就指向了全局的 window 对象,而不是 foo。
因为在调用的时候是foo.call(obj)
的形式,所以 call 方法内 this 指向的是 foo 函数,这样一来就获取到了目标函数。
然后创建一个永远都不会重复的唯一键(用 Symbol 实现),foo 函数作为值:
Function.prototype.call2 = function(target,...args){
const uniqueKey = Symbol();
target[uniqueKey] = this; // this 是 foo 函数
target[uniqueKey](...args); // 等同于 Lucy.foo(...args)
delete target[uniqueKey]; // 删除创建的临时属性
}
这样一个简易的 call 方法就完成了。
测试用例:
const foo = function (sex,address) {
console.log(this.name);
console.log(sex);
console.log(address);
}
const Lucy = {
name: 'lucy',
}
Function.prototype.call2 = function(target,...args){
const uniqueKey = Symbol();
target[uniqueKey] = this;
target[uniqueKey](...args);
delete target[uniqueKey];
}
foo.call2(Lucy,'male','hangzhou') // lucy male hangzhou
手写一个 bind 方法
bind 方法需要通过 call 方法实现,本质就是返回一个函数,然后在函数内执行 call 方法改变 this 指向。
由于返回了一个函数,在调用的时候产生了闭包,保持了对 this 的引用,解释了为什么使用 bind 后 call 和 apply 都无法再改变 this 指向。
Function.prototype.bind2 = function(target,...args){
const self = this;
return function(){
self.call( target , ...args )
}
}
测试用例:
const foo = function (sex,address) {
console.log(this.name);
console.log(sex);
console.log(address);
}
const Lucy = {
name: 'lucy',
}
Function.prototype.bind2 = function(target,...args){
const self = this;
return function(){
self.call( target , ...args )
}
}
foo.bind2(Lucy,'male','hangzhou')() // lucy male hangzhou
JS基础 - 判断 this 的指向