引言
this提供了一种更优雅的方式来隐式地传递一个对象引用,这样可以让代码更加简洁,易于复用。可以说,this的存在大大提高了JS这门语言的灵活性。
在前面的很多处代码中,我们都用到了this,但是并没有细聊this是如何工作的。
总有人说this的指向很难理解
我只想说,如果你认真看完这篇文章,请别再说不懂this的指向
对比:使用this VS 不使用this
不使用this
可以看到,我们需要手动地给函数speak传入对象
function speak(p) {
var greeting = "Hello, I am " + " " + identify(p) + ".";
console.log(greeting);
}
var Person = { name: "John" };
function identify(p) {
return p.name.toUpperCase();
}
speak(Person);
输出结果:
使用this
可以看到,speak(Person)变成了speak.call(Person),函数speak参数列表里不再需要传入对象,而是直接使用this
function speak() {
var greeting = "Hello, I am " + " " + identify(this) + ".";
console.log(greeting);
}
var Person = { name: "John" };
function identify(p) {
return p.name.toUpperCase();
}
speak.call(Person)
输出结果,跟上面一样。
我知道你有很多疑问。比如:.call()方法做了什么?
来来来,因为讲解代码要先用到,我先简单告诉你,speak.call(Person)就是将函数speak的this绑定到对象Person上,让函数speak的this指向对象Person
this的隐式绑定
深刻理解“this的指向由执行环境决定”
全局作用域下,this指向不同
在node.js中
先别管直接打印this合不合理,我们看看在node.js环境下的输出结果如何。
function fn() {
console.log(this);
}
fn();
可以看到,this指向的是global
在浏览器(V8)中
指向的是window
为什么在node.js和在浏览器(V8)跑同一份代码,输出的结果不同?
这是因为在node.js中,全局是global;而在浏览器中,全局是window对象
函数的调用方式决定this的指向
JavaScript的设计哲学
JavaScript在设计时有一个核心原则:每个函数调用都必须有一个this值。当没有明确指定this时,JavaScript需要提供一个"默认值"。
function test() {
console.log(this); // 必须有一个this,不能是undefined(非严格模式)
}
test(); // JavaScript问:this应该是什么?答:全局对象
this绑定的内部机制
函数调用的内部过程
function myFunction() {
console.log(this);
}
// 当你写 myFunction() 时,JavaScript内部实际做的是:
// 1. 确定函数对象:myFunction
// 2. 确定this值:没有明确指定,使用默认规则
// 3. 默认规则:非严格模式指向全局对象,严格模式为undefined
// 4. 调用函数:myFunction.call(全局对象)
myFunction();
// 等价于:
myFunction.call(global); // Node.js
myFunction.call(window); // 浏览器
所以,这就是清楚说明了,函数里的this出厂默认绑定全局对象,只有被其他对象调用时,才会改变this指向。也就是说,如果一直被调用,this指向就一直改变,直到指向最后的调用对象
总结一下:重要的this绑定规则
- 默认绑定:当函数被独立调用时,函数里的this指向全局
- 隐式绑定:当函数引用有上下文对象 且 被该对象调用 时,函数里的this指向这个上下文对象
- 隐式丢失:当一个函数被多层对象调用的时,函数的 this 指向最近的那一层对象
三二一,上案例
案例一:
function fn() {
console.log(this);//global
}
fn();
案例二:
function fn() {
console.log(this);//this指向global
}
var obj = {
a: 2,
fn: fn(),
}
案例三:
function fn() {
console.log(this);//this指向obj
}
var obj = {
a: 2,
fn: fn,
}
obj.fn();
案例二、三如何理解?
针对案例二:
我问你,一个函数在声明的时候没有return值,是不是就是相当于return undefined
我再问你,obj调用函数fn了吗?fn()的执行环境是什么?是obj吗?我先来告诉你,不是!
是哪?答:全局对象
针对案例三:
看到这行代码:obj.fn();
我再问你,obj调用函数fn了吗?
答:调用了。
核心解释:
上面两个解释太抽象了,我觉得是“废话”。
我就问你,在案例二:对象obj获取到函数fn的地址了吗?答:没有,只获取到undefined;
在案例三获取到了吗?答:获取到了,fn的值就是函数fn的地址,所以obj通过这个地址访问到函数fn的内部,这个过程就是调用
我知道,我知道,你看到案例一的函数fn调用长这个样子。
function fn() {
console.log(this);//global
}
fn();
你肯定想问:案例一打印的是global,它就获取到函数fn的地址了吗?答:出厂自带
接下来引入新的概念——模块包裹机制
在node.js环境的非严格模式下:
拥有模块包裹机制
当代码在Node.js模块文件(如 test.js)中执行时:
// test.js
let a = this;
console.log(a); // 输出 {}
Node.js会将模块代码包裹在一个函数中执行
(function (exports, require, module, __filename, __dirname) {
let a = this; // 此处的 `this` 指向 `module.exports`
console.log(a); // 输出 {}
}).call(module.exports, ...);
关键点:
- 包裹函数的
this被显式绑定为module.exports(初始值为空对象{}) - 模块作用域中,顶层
this不等于全局对象global,而是指向当前模块的导出对象
同样是这份代码,我们将它放在浏览器上运行
let a = this;
console.log(a);
输出的是window
关键点: 浏览器的V8引擎没有模块包裹机制,顶层作用域直接绑定到全局对象
window
进阶案例
案例一(重点):
先来看份代码,执行环境为node.js的非严格模式node.js 问:为什么输出的是undefined
var a = 1
function fn() {
console.log(this.a);//undefined
}
function fn1() {
var a = 2;
fn();
}
fn1();
答:在node.js的非严格模式下,node.js采用模块包裹机制,var声明的变量会放在模块函数里,但由于函数fn是独立调用,所以this指向global,在global里没有属性a
所以,我问你,访问对象里没有的属性,返回的是什么?答:undefined
如果在浏览器中执行这份代码呢?
答:在浏览器的执行机制眼里,用var声明对象相当于在window对象里添加属性,函数的this默认指向window,所以this.a === window.a
案例二:
如果你已经理解了案例一,那么案例二简直ez
var a = 3
function fn() {
var a = 2
function fn1() {
var a = 1;
console.log(this.a);//undefined
}
fn1();
}
fn();
案例三(重点):
请问输出的是什么?
let b = 2;
function fn() {
console.log(this.b);
}
fn();
答:undefined
为什么?我问你:let、const和var的区别在哪里?影响到this的指向了吗?回答完毕!
判断是否被对象调用
案例四:
var a = 1
function fn() {
console.log(this.a);//2
}
var obj = {
a: 2,
fn: fn,
}
obj.fn();
案例五:
var a = 1
function fn() {
console.log(this.a);//undefined
}
var obj = {
a: 2,
fn: fn(),
}
this的显式绑定
这些是官方写好的方法,简单好理解,就先直接介绍概念再讲讲案例
显示绑定:
- fn.call(obj,x,y,z...):
- 显示地将fn里的this绑定到obj上,call负责帮fn接收参数
- fn.apply(obj,[x,y,z...])
- 显示地将fn里的this绑定到obj上,apply负责帮fn接收参数,参数是数组
- fn.bind(obj,x,y,z...)
- 显示地将fn里的this绑定到obj上,bind会返回一个新的函数,新函数和bind都可以帮fn接收参数,参数零散的传入
- 返回一个新的函数,这个函数的this绑定到obj上,并且会帮fn接收参数
function fn(x, y) {
console.log(this.a, x + y);
}
var obj = {
a: 2,
}
fn.call(1, 2, 3);//undefined 5
fn.call(obj, 2, 3);//2 5
fn.apply(obj, [2, 3]);//2 5
const bar = fn.bind(obj)//优先使用bind里的参数
bar(2, 3);//2 5s
解释:
- fn.call(1, 2, 3) //undefined 5
意思就是将函数fn绑定到1上,但是1是原始类型,无法绑定属性,所以返回undefined
参数 2 3分别对应x y - fn.apply(obj, [2, 3])与上面的唯一的区别就是接收的参数是数组的形式。
- const bar = fn.bind(obj)更为灵活一些,以下的输出结果都是一样的
const bar = fn.bind(obj,3)//优先使用bind里的参数
bar(2);//2 5s
const bar = fn.bind(obj,2,3)
new的绑定
猜到能猜得到,new的绑定指向实例对象,不然实例对象怎么获取到传入的参数
箭头函数
- 箭头函数没有自己的 this,它的 this 是继承自外层作用域的 this
- 先看this函数是谁的,再看this函数是怎么被调用的
function a() {
let b = function () {
let c = () => {
let d = () => {
console.log(this)//global
}
d()
}
c()
}
b()
}
a()
总结
- 函数出厂自带this指向全局对象
- node.js的全局对象是global
- 浏览器的全局执行对象是window
- 隐式绑定核心的一句话:谁调用函数,this就指向谁
- 显式绑定熟悉方法使用
- new的绑定指向实例对象
- 箭头函数没有this
文章写作实属不易,不要吝啬点赞🌹