Javascript-this/作用域/闭包
this & 作用域链 & 闭包
this上下文context
this的核心,在大多数情况下,可以简单地理解为谁调用了函数,this就指向谁。但请注意,这里不包括通过call、apply、bind、new操作符或箭头函数进行调用的特殊情况。在这些特殊情况下,this的指向会有所不同。
this的值是在函数运行时根据调用方式和上下文来确定的。和作用域不同,作用域在代码写出来的那一刻就已经决定好了。
const o1 = {
text: "o1",
fn: function(){
console.log("o1 this",this);
return this.text;
}
};
const o2 = {
text: "o2",
fn: function(){
console.log("o2 this",this);
return o1.fn()
}
};
const o3 = {
text: "o3",
fn: function(){
// 当通过 let fn = o1.fn; 将 o1.fn 赋值给局部变量 fn 时,已经丢失了与 o1 的任何关联。
let fn = o1.fn;
return fn();
}
}
console.log("o1fn",o1.fn());
// o1调用fn,所以先打印o1对象,再打印 "o1"
console.log("o2fn",o2.fn());
// o2调用fn,所以先打印o2对象,再打印 o1对象,最后打印 "o1"
console.log("o3fn",o3.fn());
// o3调用fn,此时fn没有调用对象,所以this指向默认的window对象,window没有text属性,所以this.text返回undefined
你看明白了吗?

那提问o2.fn执行时怎么让最终的结果改成"o2"呢?
// 1. 可以直接借用函数,在o2本地去调用函数
const o1 = {
text: "o1",
fn: function(){
console.log("o1 this",this);
return this.text;
}
};
const o2 = {
text: "o2",
fn: o1.fn
};
console.log("o2fn",o2.fn())

// 2. 通过call apply bind去显示指定this
const o1 = {
text: "o1",
fn: function(){
console.log("o1 this",this);
return this.text;
}
};
const o2 = {
text: "o2",
fn: function(){
console.log("o2 this",this);
return o1.fn.call(o2) // o1.fn.call(this) o2调用fn时,this就是指向o2
}
};
console.log("o2fn",o2.fn());

call & apply & bind
- 相同点:三者都是用来改变函数调用时的this指向
- 不同:
- call
functionName.call(thisArg, arg1, arg2, ...)参数需要一个一个传递 - apply
functionName.apply(thisArg, [argsArray])参数可以用数组传递 - bind
functionName.bind(thisArg[, arg1[, arg2[, ...]]])- 返回一个新函数,在
bind()被调用时,这个新函数的this被指定为bind()的第一个参数,而其余参数将作为新函数的参数供调用时使用。
- call
call和apply都会调用函数,并返回函数调用的结果(如果有的话)。bind不会调用函数,而是返回一个新的函数,这个新的函数被调用时,才会执行原始函数,并且具有指定的this值和预置的参数。- 使用场景
- 如果知道要传递的参数,并且想要立即调用函数,那么可以使用
call或apply。 - 如果想要创建一个新函数,这个函数在被调用时具有特定的
this值和预置的参数,那么可以使用bind。
- 如果知道要传递的参数,并且想要立即调用函数,那么可以使用
有时我们会遇到要手写这三个函数的情况,我们可以先写一个大致的框架,列出函数的输入输出,然后再向框架里填充内容。
call
function hello(start, end) {
return start + ', ' + this.name + end;
}
const person = { name: 'Alice' };
// 使用 call 调用 hello 函数,并将 this 绑定到 person 对象
const message = hello.call(person, 'Hello', '!');
console.log(message); // 输出 "Hello, Alice!"
手写call
输入:一个上下文,可选参数(一个一个传递)
输出:函数执行的结果
/*
function speak() {
console.log(this.name,'can speak!'); // Alice can speak
}
const obj1 = {
name: 'Alice'
}
speak.myCall(obj1);
*/
Function.prototype.myCall = function (context,...args) {
// 边界检测,如果context没传则将上下文替换成全局对象window||global
context = context || window;
// 将调用myCall的函数(这里指的是speak)作为新的上下文(调用myCall时传入的obj1)的属性值添加到上下文中
// 注意:这里我们用context.fn作为中间变量来调用函数
context.fn = this; // // 将this赋值给context的一个属性
const result = context.fn(...args); // 使用context作为上下文调用函数
delete context.fn; // 清理环境,避免内存泄漏
return result;
}
在这个例子中,speak 是被调用的函数,所以 this 在 myCall 内部指向 speak 函数。而 context 是我们传递给 myCall 的 obj1 对象,我们希望在 speak 函数内部使用 obj1 作为 this 上下文。因此,我们将 speak 函数作为 obj1 的一个方法(临时)来调用它,实现了改变 this 上下文的效果。
中途打断点可以看到context的值如图

apply
// Math.max() 函数不接受数组作为参数。它接受任意数量的数字参数,并返回这些参数中的最大值。所以这是最适合用于演示apply的函数
function max(numbers) {
return Math.max.apply(null, numbers);
}
const maxNum = max([1, 2, 3, 4, 5]);
console.log(maxNum); // 输出 5
手写apply
经过上面的例子手写了call之后,手写apply就没什么难的了,因为它俩就接受参数的方式不同而已。apply接受的是一个数组
Function.prototype.myApply = function (context,argumentsArr) {
context = context || window;
context.fn = this;
// 如果argumentsArr存在则将其内容作为参数传递
const result = argumentsArr ? context.fn(...argumentsArr) : context.fn();
delete context.fn;
return result
}
// 将上面的Math.max.apply改成myApply是一样的效果
bind
比如当我们想要确保某个函数总是以特定的上下文来执行时。例如,在事件处理器、回调函数和定时器中,我们需要绑定 this 上下文,确保函数总是能够正确地访问和操作我们期望的对象。
1. 事件处理器中的 this 绑定
在事件处理器中,this 通常指向触发事件的元素,而不是我们期望的对象。使用 bind 可以确保 this 指向正确的对象。
function Button() {
this.name = 'My Button';
this.handleClick = function(event) {
console.log(this.name + ' was clicked!'); // 'this' 指向Button实例
};
const buttonElement = document.getElementById('btn');
buttonElement.addEventListener('click', this.handleClick.bind(this));
}
const myButton = new Button();
// 输出:My Button was clicked!
// 如果改成下面这个则不会输出name,只会输出 was clicked!
// buttonElement.addEventListener('click', this.handleClick);
2. 回调函数中的 this 绑定
在异步操作或回调函数中,this 的值可能会变化。使用 bind 可以确保 this 的值在回调函数执行时保持不变。
function User(firstName) {
this.firstName = firstName;
this.fetchData = function(url) {
// 假设fetch是一个模拟的异步函数
fetch(url)
.then(response => response.json())
.then(data => {
console.log(this.firstName + ' fetched data: ', data); // 'this' 指向User实例
}).bind(this) // 使用bind确保this指向User实例
.catch(error => console.error('Error:', error));
};
}
const user = new User('Alice');
user.fetchData('https://api.example.com/data');
注意:在现代JavaScript中,通常使用箭头函数来自动绑定 this,因为箭头函数不绑定自己的 this,而是捕获其所在上下文的 this 值。
3. 预设参数
使用 bind 可以预设函数的参数。这在创建可复用的函数时非常有用。
function list() {
return Array.prototype.slice.call(arguments);
}
const list1 = list(1, 2, 3); // [1, 2, 3]
// 创建一个新的函数,预设第一个参数为'boys'
const listWithItems = list.bind(null, 'boys');
const list2 = listWithItems(1, 2, 3); // ['boys', 1, 2, 3]
在这个例子中,listWithItems 是 list 函数的一个新版本,将 'boys' 作为第一个参数。当我们调用 listWithItems(1, 2, 3) 时,它实际上是在调用 list('items', 1, 2, 3)。
4. 绑定到特定的上下文
有时我们可能希望将函数绑定到特定的对象,以便在其他地方调用它时,它总是以该对象为上下文。
const obj = {
age: 10,
getAge: function() {
return this.age;
}
};
const unboundGetAge = obj.getAge;
console.log(unboundGetAge()); // undefined,因为this没有绑定到obj
const boundGetAge = obj.getAge.bind(obj);
console.log(boundGetAge()); // 10,因为this被绑定到obj
在这个例子中,unboundGetAge 在调用时没有绑定 this,所以它的 this 值是 window。而 boundGetAge 则被绑定到 obj 对象,因此它总是返回 obj.age 的值。
手写bind函数
// 手写bind
// 输入:一个新的this上下文,以及可选的参数列表
// 输出:一个新的函数,这个新函数被调用时会将this设置为指定的值,并且将参数列表与bind调用时提供的参数合并
Function.prototype.myBind = function (context,...initialArgs){
// 1.保存调用 myBind 方法的原始函数。
const self = this;
// 2.返回一个新函数
return function F(...boundArgs) {
// 3.判断函数是否以构造函数的方式调用 这一段我还没理解
if (this instanceof F) {
// 如果是,那么 this 会指向一个新创建的对象,而不是我们提供的 context。
// 我们就使用new和原始函数self来调用
return new self(...initialArgs,...boundArgs);
}
// 否则,直接调用原始函数self,并传入context作为this,以及合并后的参数
return self.apply(context,[...initialArgs,...boundArgs]);
}
}
/*
function hello(start, end) {
return start + ', ' + this.name + end;
}
const obj = { name: 'Alice' };
const boundHello = hello.myBind(obj, 'Hello');
console.log(boundHello('!')); // 输出 "Hello, Alice!"
*/
上面这段代码执行栈如下图,context就是我们传入的obj对象,initialArgs是我们在调用bind是传入的'Hello',boundArgs是我们调用返回的新函数boundHello时传入的参数。

当我们使用 bind 方法(无论是原生的 Function.prototype.bind 还是手写实现的 myBind)时,我们实际上是在创建一个新的函数,这个函数被“绑定”到了特定的 this 上下文(在这个例子中是 obj 对象)以及一些预先设定的参数(在这个例子中是 'Hello')。
这个新函数(我们称之为 boundHello)现在可以独立使用,并且每次调用它时,都会以我们指定的 this 上下文(obj)和预先设定的参数('Hello')来调用原始的 hello 函数。
作用域与作用域链
作用域:变量和函数在代码中可以访问和可见的范围。
function test() {
var x = '内部变量'
}
test()
console.log(x) // x is not defined
x只在test函数的作用域内,所以在全局作用域下去访问x就会报错
总结: 作用域是一个独立的区域,它定义了在该区域内声明的变量和函数的可访问性范围。
目的: 主要目的是隔离变量,确保它们不会无意中泄露到外部作用域,从而避免命名冲突和数据污染。
作用:
- JavaScript能够控制哪些代码块可以访问哪些变量和函数,提高了代码的安全性、模块性和可维护性。
- 同时,作用域也是实现闭包等高级特性的基础,它允许函数记住并访问其外部作用域中的变量,即使外部函数已经执行完毕。
-
全局作用域:在代码的最外层定义的变量和函数具有全局作用域,这意味着它们可以在代码的任何地方被访问。在浏览器环境中,全局作用域中的变量和函数会成为window对象的属性和方法。
![image-20240831013731847]()
-
函数作用域:在函数内部定义的变量和函数(不使用let或const)具有函数作用域,也称为局部作用域。这些变量和函数只能在定义它们的函数内部被访问。
![image-20240831014347350]()
同理,我们直接访问内部的Play函数也会报错
![image-20240831014520608]()
var:它声明的变量具有函数作用域或全局作用域(如果变量是在函数外部声明的)。此外,var声明的变量存在变量提升(hoisting)现象,即无论变量在何处声明,都会被视为在函数或全局作用域的顶部声明(声明可以在使用后)。let:ES6引入的let关键字用于声明块级作用域的变量。与var不同,let声明的变量仅在它们被声明的块(如{}块、if语句、for循环等)内部有效。此外,let声明的变量不会被提升(先声明再使用)。const:与let类似,const也是ES6引入的,但它用于声明常量。一旦一个常量被赋值,它的值就不能被重新赋值(如果常量是一个对象,则可以修改对象的属性,但不能将常量重新指向另一个对象)。const声明的常量也具有块级作用域,并且不会被提升。
变量提升:如下图所示,可以将
var test = 's'拆解成先声明,再赋值,所以第一次console.log时是undefined而不是下面红色报错![image-20240831021143852]()
-
块级作用域:ES6引入了let和const关键字,使得变量可以在块级作用域(如if语句、for循环或{}块)中定义。这些变量只能在定义它们的块内部被访问。
块级作用域很容易理解,一对花括号
{}包裹的就是一个块![image-20240831014916468]()
-
模块作用域:ES6还引入了模块的概念,允许将相关的代码组织在一起,并通过export和import语句在模块之间共享变量、函数、类等。每个模块都有自己的作用域,模块内部定义的变量和函数默认只能在该模块内部被访问,除非它们被显式导出。
// math.js // 使用 export 关键字导出函数 export function add(x, y) { return x + y; } // 注意:这个函数没有被导出,因此只能在 math.js 内部访问 function privateFunction() { console.log('这是一个私有函数,只能在 math.js 内部调用'); } // 直接在模块中使用的变量也是模块作用域的 let privateVar = '我是一个私有变量'; privateFunction() console.log(privateVar); // 可以在模块内部访问// index.js import {add} from './math.js' // 使用导入的函数 console.log(add(2, 3)); // 输出: 5 // 尝试访问 math.js 中的私有函数和变量会导致错误 // console.log(privateFunction()); // Uncaught ReferenceError: privateFunction is not defined // console.log(privateVar); // Uncaught ReferenceError: privateVar is not defined
作用域理解之后再看看作用域链
let a = 'global'
console.log(a) // global
function outFunc() {
let b = 'hello'
console.log(b) // hello
midFunc()
console.log(c) // c is not defined
function midFunc() {
let c = 'world'
console.log(c) // world
innerFunc()
function innerFunc() {
d = '!' // 直接赋值 此时d被放到了全局作用域
console.log(d) // !
console.log('test',b) // test hello
}
}
}
outFunc()
console.log(d) // 输出! 注意这里是可以取到d的
作用域向上查找,内部可以拿到外部的变量,反之则行不通 => 作用域链:作用域的集合,子集可以访问父集,父集不能访问子集





浙公网安备 33010602011771号