this 是什么
js 中 this 指的是函数的执行主体。
我们需要注意的是,只有函数执行的时候才能确定 this 到底指向谁。
涉及 this 指向问题的场景汇总
1. 事件绑定
给某个Dom元素的某事件行为绑定函数,当事件行为被触发时,绑定的函数会执行,此时函数中的 this 一般都是当前操作的元素本身。
案例: 我们给 document 的点击事件绑定一个函数,然后用鼠标点击页面触发执行事件处理函数。代码如下:
// 1 使用 `onxxx` 方式给它绑定函数
document.onclick = function() {
console.dir(this); // #document
}
// 2 使用事件监听方式给它绑定函数
document.addEventListener('click', function() {
console.log(this); // #document
});
// 3 特殊情况: IE6~8 使用 attachEvent 方式绑定事件
document.body.attachEvent('onclick', function () {
// this -> window/undefined
});
2. 普通函数
普通函数执行时,主要是先看 .
,函数调用语句前面是否有 .
- 如果有
.
,那么.
前面是谁,this 就指向谁。 - 如果没有
.
,那么 this 就指向 window / undefined
window / undefined
非严格模式下是:window
严格模式下是:undefined 后面这样写也是一个意思
普通函数执行也有几种特殊的存在:
- 自执行函数,执行时 this 一般指向 window / undefined 。
- 回调函数, 执行时 this 一般来讲都是指向 window / undefined 。但是如果在在执行回调函数时,在函数内部做了特殊处理,是可以改变 this 指向的。
- 在括号表达式中,只会取出最后一项来执行。如果最后一项是函数,函数执行时 this 指向 window / undefined 。如果最后一项不是函数,执行的话会报错。
案例:普通函数执行,看前面是否有 .
const fn = function fn(x, y) {
console.log(x, y, this);
};
fn(10, 20)
// 10 20 Window ---》this 指向 window
let obj = {
name: 'zhufeng',
fn: fn
};
obj.fn(10, 20)
// 10 20 { name: "xiaoming", fn: ƒ }
// fn(10, 20) 函数执行前有 . ,. 前面是 obj
// ---》this 指向 obj
案例:执行同一个方法,this 指向却不同
let arr = [];
arr.push(2); //push方法中的this->arr
arr.__proto__.push(); //this->arr.__proto__
Array.prototype.push(); //this->Array.prototype
案例:回调函数中的 this
// 1. 把一个函数当作参数传给另外一个函数
const fn = function fn(callback) {
// callback -> 传递进来的函数
callback(); // 回调函数中的this->window
callback.call(1); //改变了 this 指向,this->1
};
fn(function () {
console.log(this);
});
// 2. forEach() 方法的第2个参数,可以指定回调函数中的 this
let arr = [10, 20, 30],
obj = {
name: 'zhufeng'
};
arr.forEach(function (item) {
console.log(item, this); //window
});
arr.forEach(function (item) {
console.log(item, this); //obj
}, obj);
案例:括号表达式 现实开发中已经很少这么用了
'use strict'
const fn = function fn(x, y) {
console.log(x, y, this);
};
let obj = {
name: 'zhufeng',
fn: fn
};
(10, 20, obj.fn)();
// 最后一项 obj.fn 是函数,可以执行
// 非严格模式下输出:
// undefined undefine Window
// 严格模式下输出:
// undefined undefined undefined
// (10, obj.fn, 30)();
// Uncaught TypeError: (10, obj.fn, 30) is not a function
定时器 setTimeout 的回调函数中的this
let obj = {
name: '小明',
// 为对象的属性,添加一个函数类型的值
// 这样写效果类似于:“fn:function(){}”
// 这样写的函数没有 prototype 原型对象
fn() {
console.log(this); // obj {name: "小明", fn: ƒ}
setTimeout(function () {
this.name = '王鹏';
console.log(this); // Window {name: "王鹏"...}
console.log(obj); // {name: "小明", fn: ƒ}
}, 1000);
}
};
obj.fn();
基于上述代码,我们想在 setTimeout 的回调函数内部,修改obj 的 name 的属性值改为 '王鹏',该怎么做呢?
// 方案1:使用变量 _this 缓存 fn() 执行时所属上下文中的 this
let obj = {
name: '小明',
fn() {
console.log(this); // {name: "小明", fn: ƒ}
let _this = this;
setTimeout(function () {
_this.name = '王鹏';
console.log(this); // this->window
}, 1000);
}
};
obj.fn();
// 方案2:setTimeout 的回调函数,使用箭头函数形式
let obj = {
name: '小明',
fn() {
console.log(this); // this -> {name: "小明", fn: ƒ}
setTimeout(() => {
this.name = '王鹏';
console.log(this); // this-> {name: "王鹏", fn: ƒ}
console.log(obj); // obj -> {name: "王鹏", fn: ƒ}
}, 1000);
}
};
obj.fn();
3. 构造函数「函数执行前加 new 关键字」
构造函数体中的 his 指向当前 构造函数/类 的实例。 构造函数执行与普通函数执行的核心区别在于,是否加 new 关键字执行。
4. 箭头函数(含块级上下文)
箭头函数中(含块级上下文)没有自己的 this ,用到的 this 都是它上级上下文「宿主环境」中的 this 。
箭头函数非常好用,但是不能乱用
- 不涉及THIS,爱咋用咋用,
- 但是一但涉及THIS问题,三四后行
案例: 箭头函数中没有自己的 this
let obj = {
name: '小明',
fn: () => {
console.log(this);
}
};
obj.fn(); //this -> window
obj.fn.call(100); //this -> window
4. 强制改变this 指向
基于Function.prototype上的call/apply/bind
三个可以强制改变函数中的 this 指向
对箭头函数使用这3个方法,也不会生效,因为他们没有自己的 this。
三者之间的区别
- apply() 和 call()
- 相同点:都是
立即执行调用它们的函数
,并改变该函数中的 this 指向。 - 不同点:唯一区别就是传参的方式不同。call() 是一项一项的给函数传参。apply() 要求所有参数
必须以数组形式
编写,但是最后的结果和一样,也是一项项传递给函数。
- apply() , call() vs bind()
- apply() 和 call() 的共同特点是:会立即执行调用它们的函数。
- bind() 是预处理处理,不会立即执行调用它们的函数,只是会预先处理 this ,把信息预先存储好而已。
案例:
有代码如下,满足下列需求:
- 点击 document 元素后,执行 fn 函数,并为其传递 10,20 两个参数。
- 并且让 fn 函数执行过程中的 this 指向 obj
const fn = function fn(x, y) {
console.log(x, y, this);
}
let obj = {
name: '我是obj'
}
document.onclick = fn();
// => x, y, this : undefined, undefined, window
// 这样写的意思是把 fn 执行,
// 并把返回值赋值给 document 的点击事件
document.onclick = fn(10, 20);
// => x, y, this : 10, 20, window
// 这样写不符合需求。
// 这样写的意思是给 fn 函数传参 10 和 20 , 并把它执行
// 把返回值赋值给 document 的点击事件
document.onclick = fn;
// 这样写 document 的点击事件被触发时,才会执行 fn
// 此时 fn 中的 this 指向当前被操作元素本身,即 document,
// 浏览器还会默认给事件函数传递一个实参:事件对象 。这个实参默认由函数中的第一个形参接收。
// => x, y, this : MouseEvent {...}, undefined, document
// 不符合需求
document.onclick = fn.call(obj, 10, 20 );
// => x, y, this : 10 20 {name: "我是obj", fn: ƒ}
// this 指向改变了,指向了 obj 这一点符合要求
// call() 会立即执行函数,这一点不符合需求
// 不符合需求
document.onclick = obj.fn(10, 20);
// 前提:给 obj 添加一个 fn 的属性,其属性值为 fn 函数
// 这样写符合需求
// => x, y, this : 10 20 {name: "我是obj", fn: ƒ}
document.onclick = function(ev) {
// fn(); // this : window
fn.call(obj, 10, 20); // this: obj
}
// 这样写符合需求
// document 的点击事件被触发时,执行的是匿名函数,
// 在匿名函数中基于 call() 去执行 fn ,改变this 指向 obj,并传参。
// => x, y, this : 10 20 {name: "我是obj", fn: ƒ}
document.onclick = fn.bind(obj, 10, 20 );
// 这样写符合需求
// => x, y, this : 10 20 {name: "我是obj", fn: ƒ}
// bind() 是预先处理 this。bind() 并不立即执行函数,只是把信息预先处理好。
// bind() 是执行了,其返回值是一个匿名函数,匿名函数中,已经存储了 this,传递的参数等信息。
仿写一个bind 方法
bind 方法:预先处理 this, 并不马上执行我们需要执行的函数。比如案例中为点击事件的绑定的方法。需要在点击行为发生后才执行绑定的方法,并不是马上执行函数。
使用到了 柯理化函数思想(预处理思想):函数执行产生一个闭包,预习把一些值存储起来,供其下一级上下文中使用。
const fn = function fn(x, y) {
console.log(x, y, this);
}
let obj = {
name: '我是obj',
fn: fn
}
Function.prototype._bind = function _bind(context, ...params) {
// this: fn context : obj params 需要传递给 fn 函数的参数
let self = this;
return function(...args) {
// args :接收匿名函数执行时,会接收到的实参比如:
// 事件处理函数执行时,会接收到一个ev 事件对象
// 合并参数
params = params.concat(args)
// 基于call,会立即执行函数,并改变其this指向
self.call(context, ...params);
}
}
document.onclick = fn._bind(obj, 10, 20);
// 10 20 {name: "我是obj", fn: ƒ}
仿写一个 call 方法
const fn = function fn(x, y) {
console.log(x, y, this);
return x+y
}
let obj = {
name: '我是obj'
// fn: fn
}
Function.prototype._call = function _call(context, ...params) {
// 如果 context 接收到的是个原始值类型的值,比如: 1
// 原始值类型的值是无法设置属性的,否则会报错
// 假如接收到的是 null 或者 undefined, 我们把 context - > window
// 假如接收到的是 其他原始值类型的值 , 我们把原始值变成其对应的对象类型值
if(context == null ) context = window;
if(!/^(object|function)$/.test(typeof context)) context = Object(context);
// this -> fn context -> obj params ->需要传递给fn的参数
// fn 和 obj 本身毫无关系
// 要使 this -> obj,我们需要将他们关联起来
// 如果是 obj.fn() 这样执行,this 就是 obj
// 那我们把 fn 赋值给 obj 的某个属性即可。
// context.fn = fn;
// context.fn(...params); // x, y, this -> 10 20 {name: "我是obj", fn: ƒ}
// // 我们发现,obj 中多了一个 fn 属性,使原本的数据发生了变化,
// // 而我们并不希望如此,所以需要把新增的 fn 属性移除
// delete context.fn;
// 但是假如原本的 obj 中有一个 fn 属性,如果我们这样将其移除,也会使原数据发生改变
// 所以我们在给 obj 添加属性时,添加一个 由 Symbol 生成的唯一值作为属性名的话,不会出现这种情况了
let key = Symbol('key'),
result;
context[key] = this;
// result 接收函数的 返回值
result = context[key](...params);
delete context[key];
return result;
}
fn._call(obj, 10, 20);
// 10 20 {name: "我是obj", Symbol(key): ƒ}
// 控制台展开后 Symbol(key) 属性是没有的