JS基础三座大山之二一作用域和闭包(1)
学习闭包呢,首先要了解清楚闭包的前置知识,例如作用域,this,垃圾回收等基础知识,只有搞清楚了这些基础的知识,在学习闭包时才不会由过多的困惑和疑问,由于前置知识讲解完也是会有很长的篇幅了,那么最后闭包的介绍讲解会放在 JS基础三座大山之二一作用域和闭包(2) 中讲解
带着以下几个问题开始学习吧
- 什么是作用域?
- 什么是作用域链?
- 垃圾回收
- this 有几种赋值情况
1. 作用域和作用域链
JavaScript 中的作用域,指变量和函数的可访问范围,它决定了代码在不同区域中如何和何时可以访问变量和函数。其共有三种作用域:
- 全局作用域:在代码中任何地方都能访问到的变量或函数。window、document
- 局部(函数)作用域: 函数内部的作用域,函数之外不可用函数内部变量
- 块级作用域:(该作用域在 ES6 引入 let 和 const 后才出现)只在定义它们的代码块中访问(如:if、for、while)到的变量或函数。
当代码需要访问变量时,JavaScript 解释器会首先在当前作用域中查找是否有该变量的定义
。如果没有找到,解释器会继续在上一层作用域中查找,这个查找过程会一直持续到全局作用域
。如果在全局作用域中仍然没有找到该变量,则会抛出引用错误(ReferenceError)。这种解析变量的机制(多个作用域层次顺序连接而成的链),就是作用域链。
1.1 补充 let、var、const 的区别
var
声明的变量仅能为全局变量或局部变量。在函数外(全局)声明的变量为全局变量,在函数内声明的变量为局部变量。let(const)
声明的变量可以为全局变量、局部变量、块级作用域变量,依赖于它们所处的位置。- 由于
var 声明的变量会无视块级作用域
,所以现在通常都是用 let 和 const 声明变量。 var 有变量提升
,但如果在声明前调用则为 undefined;可以重复声明、可以重新赋值、可以没有初始值(初始值:undefined)。let 没有变量提升
,必须先声明后使用;不可以重复声明、可以重新赋值、可以没有初始值(初始值:undefined)。const 没有变量提升
,必须先声明后使用;不可以重复声明、不可以重新赋值、必须有初始值。- 函数声明会被提升,可以在声明前调用。
- 函数表达式的行为取决于使用的是 var、let 还是 const。
什么是变量提升?
先用后声明,且不会报错
console.log(name); // 输出:undefined(不会报错!)
var name = "小明";
console.log(name); // 输出:"小明"
1.2 基本数据类型和引用数据类型的区别
基本数据类型
包括:String、Number、Boolean、Null、Undefined、Symbol(ES6 新增)、BigInt(ES2020 新增)。
- 存储位置:基本数据类型的值直接存储在栈(Stack)内存中。
- 访问方式:访问基本数据类型的值是直接访问其存储在栈内存中的值。
- 复制行为:复制基本数据类型的变量时,会在栈内存中创建一个新值,复制的是值本身,因此两个变量互不影响。
引用类型
包括:Object(包括所有的对象,如 Array、Function、Date、RegExp 等)。
- 存储位置:引用类型的值存储在堆(Heap)内存中,而变量实际上存储的是指向堆内存中该对象的地址(即引用)。
- 访问方式:访问引用类型的变量时,首先从栈内存中获取该对象的地址引用,然后通过引用找到堆内存中的对象。
- 复制行为:复制引用类型的变量时,复制的是对象的地址引用,而不是对象本身,因此两个变量会指向堆内存中的同一个对象。对其中一个变量所引用的对象的修改会影响到另一个变量。(这也是为什么需要深克隆的原因)
2. 垃圾回收
垃圾回收机制
垃圾回收是一种自动内存管理的机制,它的目的是帮助程序自动回收不再使用的内存资源,减少内存泄漏的风险,从而避免程序手动管理内存的复杂性和出错的可能性。
那 JS 是怎么判断哪些是不再使用的内存资源呢?
通常是通过跟踪对象的引用来实现的。如果一个对象没有任何引用指向它(即不可达),那么这个对象就被认为是“不再需要的”,垃圾回收器就可以回收这块内存。
(这一块讲的比较简单,想要深度了解垃圾回收机制可以查找其他资料)
3. this
JavaScript 中的 this 是一个非常重要且容易困惑的概念,主要用于引用执行上下文。它像一个"动态指针",总是指向当前执行代码的上下文对象。通俗地说,this 的值取决于函数被调用的方式,而不是定义的位置。
举个生活化的比喻:
想象你在不同场合被叫名字:
在家妈妈叫你 → "宝贝"
在学校老师叫你 → "同学"
在公司老板叫你 → "员工"
你(this)还是你还是同一个人,但称呼会根据谁在叫你(调用环境) 而变化。
3.1 this 的 几种种常见指向(重点!)
在强调一遍:this 取什么值:是在函数执行
的时候确认的,不是在函数定义的时候确认的
- 作为普通函数 ->this指向 window(直接调用函数 → 指向全局对象)
function showThis() {
console.log(this);
}
showThis(); // 浏览器中输出 window,Node.js 中输出 global
- 使用call、apply、bind ->this指向 传入的对象
function greet() {
console.log("你好," + this.name);
}
const person = { name: "小明" };
greet.call(person); // "你好,小明" (强制 this 指向 person)
- 作为对象方法被调用 ->this指向 调用的对象
const phone = {
brand: "Apple",
show: function() {
console.log(this.brand); // this 指向 phone 对象
}
};
phone.show(); // 输出 "Apple"
- 在class方法中被调用 ->this指向 实例对象(即构造函数中 → 指向新创建的对象)
function Car(model) {
this.model = model; // this 指向新创建的实例
}
const myCar = new Car("Tesla");
console.log(myCar.model); // "Tesla"
- 箭头函数 ->this指向 上级作用域(箭头函数没有自己的 this,它会"继承"外层作用域的 this:)
const watch = {
brand: "Rolex",
show: () => {
console.log(this.brand); // 箭头函数继承外层 this(通常是 window)
}
};
watch.show(); // 输出 undefined
- 事件处理函数中的 this
在 DOM 事件中,this 指向触发事件的元素:
<button onclick="console.log(this)">点击</button>
<!-- 点击时输出 <button> 元素 -->
3.2 改变 this 指向的方法
this 的指向经常因为不同的执行环境而改变。为了解决这个问题,JavaScript 提供了多种方法来明确指定 this 的指向。 JavaScript 中常用的四种改变 this 指向的方法:bind
、call
、apply
和箭头函数
。
1. bind() 方法
bind()
方法返回一个新函数
,这个新函数的 this 值会被永久绑定为指定的对象。常用于当我们需要在事件回调或异步操作中确保 this 的指向不变时。
例1:
const obj = {
name: 'Bound Object'
};
function showName() {
console.log(this.name);
}
const boundShowName = showName.bind(obj);
boundShowName(); // 输出:'Bound Object'
例2:
var Person = {
name: "zhangsan",
age: 19
}
function aa(x, y) {
console.log(x + "," + y);
console.log(this);
console.log(this.name);
}
aa(7,8) // 输出:7,8 this指向的是window 空
aa.bind(Person, 4, 5); //只是更改了this指向,没有输出,因为没有调用函数
aa.bind(Person, 4, 5)(); //this指向Person,输出:4,5 Person{}对象 zhangsan
2. call() 方法
call()
方法用于立即调用函数,并指定函数内部的 this。与 bind() 不同,call() 直接执行函数,不会返回新的函数。
例1:
const obj = {
name: 'Call Object'
};
function showName() {
console.log(this.name);
}
showName.call(obj); // 输出:'Call Object'
例2:
var Person = {
name: "zhangsan",
age: 19
}
function aa(x, y) {
console.log(x + "," + y);
console.log(this);
console.log(this.name);
}
aa(4, 5); //this指向window--4,5 window 空
aa.call(Person, 4, 5); //this指向Person--4,5 Person{}对象 zhangsan
传递参数
call()
的一个重要特点是,除第一个参数外,后续所有参数都将依次传递给被调用的函数。例如:
const obj = {
name: 'Call Object'
};
function greet(greeting, punctuation) {
console.log(greeting + ', ' + this.name + punctuation);
}
greet.call(obj, 'Hello', '!'); // 输出:'Hello, Call Object!'
3. apply() 方法
apply()
和 call()
类似,但它的区别在于,apply()
接受一个参数数组,而不是逐个列出参数。
const obj = {
name: 'Apply Object'
};
function showNameWithAge(age) {
console.log(`${this.name}, Age: ${age}`);
}
showNameWithAge.apply(obj, [30]); // 输出:'Apply Object, Age: 30'
var Person = {
name: "zhangsan",
age: 19
}
function aa(x, y) {
console.log(x + "," + y);
console.log(this);
console.log(this.name);
}
aa.apply(Person, [4, 5]); //this指向Person--4,5 Person{}对象 zhangsan
传递参数数组
apply()
方法非常适合当我们有一组参数需要传递给函数时使用:
const numbers = [5, 6, 2, 3, 7];
const max = Math.max.apply(null, numbers);
console.log(max); // 输出:7
4. 箭头函数
在 JavaScript 中,箭头函数通过 词法作用域(Lexical Scope) 绑定 this,它不会创建自己的 this 上下文,而是直接继承外层作用域的 this 值。这与传统函数(会动态绑定 this)形成鲜明对比。下面通过具体示例说明箭头函数如何“改变” this 指向:
- 示例 1:解决回调函数的 this 丢失问题
传统函数中,嵌套函数的 this 默认指向全局对象(如 window),导致错误:
const obj = {
name: "Alice",
traditionalFunc: function() {
setTimeout(function() {
console.log(this.name); // 输出:undefined(this 指向 window)
}, 100);
}
};
obj.traditionalFunc(); // 传统函数导致 this 丢失
箭头函数继承外层 this:
const obj = {
name: "Alice",
arrowFunc: function() {
setTimeout(() => {
console.log(this.name); // 输出:"Alice"(继承 arrowFunc 的 this)
}, 100);
}
};
obj.arrowFunc(); // 箭头函数正确绑定 obj
关键点:
箭头函数没有自己的 this,它直接继承外层 traditionalFunc 的 this(即 obj),从而修复了 this 指向。
- 示例 2:无法通过 call/apply/bind 修改箭头函数的 this
箭头函数的 this 在定义时已固化,无法动态改变:
const obj1 = { name: "Alice" };
const obj2 = { name: "Bob" };
// 传统函数可动态修改 this
function traditionalFunc() {
console.log(this.name);
}
traditionalFunc.call(obj1); // "Alice"(动态绑定)
traditionalFunc.call(obj2); // "Bob"
// 箭头函数固化 this
const arrowFunc = () => {
console.log(this.name);
};
arrowFunc.call(obj1); // 输出:""(this 指向定义时的全局对象)
arrowFunc.call(obj2); // 同上(无法修改)
- 示例 3:对象方法中的陷阱
箭头函数在对象方法中可能绑定到错误的 this:
const obj = {
name: "Alice",
arrowMethod: () => {
console.log(this.name); // 输出:""(this 指向全局对象)
},
traditionalMethod: function() {
console.log(this.name); // 输出:"Alice"
}
};
obj.arrowMethod(); // 错误!this 不是 obj
obj.traditionalMethod(); // 正确
原因:
箭头函数在对象字面量中定义时,其外层作用域是全局作用域(非函数作用域),因此 this 指向全局对象。
- 示例 4:构造函数中的差异
箭头函数不能用作构造函数,且会固定继承定义时的 this:
function Person(name) {
this.name = name;
this.traditionalMethod = function() {
console.log(this.name);
};
this.arrowMethod = () => {
console.log(this.name);
};
}
const alice = new Person("Alice");
alice.traditionalMethod(); // "Alice"(this 指向实例)
alice.arrowMethod(); // "Alice"(继承构造函数中的 this)
const bob = { name: "Bob" };
alice.arrowMethod.call(bob); // 依然输出 "Alice"(无法修改 this)
箭头函数绑定 this 的核心规则:
-
词法作用域:
箭头函数的 this 在定义时确定,继承自外层第一个传统函数的 this(若无则指向全局对象)。
-
不可动态修改:
call(), apply(), bind()
无法改变箭头函数的 this。 -
无独立 this:
箭头函数自身不提供 this 绑定,直接使用外层 this。
何时使用箭头函数固定 this?
-
需要保持回调函数的 this 与外层一致时(如 setTimeout、事件监听器)。
-
需明确避免 this 被动态修改的场景。
-
避免在对象方法、构造函数、原型方法中使用(需动态 this 的场景)。
箭头函数通过静态绑定 this,解决了传统函数动态 this 引发的常见问题,但需根据场景谨慎使用。
闭包的介绍讲解会放在 JS基础三座大山之二一作用域和闭包(2) 中讲解