目录
JavaScript 进阶
1. 作用域
作用域(Scope)是指程序中定义变量的区域。JavaScript 中主要有局部作用域和全局作用域。
1.1 局部作用域
局部作用域指的是在特定的代码块或函数中定义的变量,这些变量在外部不可访问。
1.1.1 函数作用域
函数作用域是指变量在函数内部声明,它们仅在函数内部可见。
function myFunction() {
var x = 10; // x 仅在函数内部可见
console.log(x); // 10
}
myFunction();
console.log(x); // Uncaught ReferenceError: x is not defined
1.1.2 块作用域
块作用域是指变量在特定的代码块(例如 if
、for
语句)中定义,它们仅在该代码块内部可见。let
和 const
关键字用于声明块作用域变量。
if (true) {
let y = 20; // y 仅在块内部可见
console.log(y); // 20
}
console.log(y); // Uncaught ReferenceError: y is not defined
1.2 全局作用域
全局作用域是指在任何地方都可以访问的变量。这些变量在代码的任何部分都可见。
var z = 30; // 全局变量
function myFunction() {
console.log(z); // 30
}
myFunction();
console.log(z); // 30
1.3 作用域链
当一个变量被引用时,JavaScript 引擎会从当前作用域开始查找变量,如果没有找到,则继续查找父级作用域,直到找到该变量或达到全局作用域。这种查找机制称为作用域链。
var globalVar = "global";
function outerFunction() {
var outerVar = "outer";
function innerFunction() {
var innerVar = "inner";
console.log(innerVar); // "inner"
console.log(outerVar); // "outer"
console.log(globalVar); // "global"
}
innerFunction();
}
outerFunction();
1.4 垃圾回收(Garbage Collection, GC)
垃圾回收是编程语言的一种特性,旨在自动释放不再使用的内存,使开发者无需手动管理内存分配和释放。
1.4.1 内存生命周期
- 分配:当创建对象、变量时分配内存。
- 使用:执行代码,读写分配的内存。
- 释放:当对象不再被引用时,释放内存。
1.4.2 标记和清除算法(Mark-and-Sweep)
标记阶段
垃圾回收器会遍历所有的根对象(root objects,例如全局对象、局部变量、闭包中的变量等),并标记所有从根对象可达的对象。
清除阶段
在标记完成后,垃圾回收器会清除所有未被标记的对象,即这些对象不再被引用的对象,并释放其占用的内存。
1.4.3 引用计数(Reference Counting)
原理
每个对象维护一个引用计数,记录有多少地方引用了该对象。当引用计数变为0时,表示该对象不再被使用,可以被回收。
缺陷
引用计数无法处理循环引用的问题,例如两个对象相互引用,导致无法被回收。
1.4.4 垃圾回收策略
分代垃圾回收(Generational Garbage Collection)
将内存分为几代,如新生代(Young Generation)和老生代(Old Generation)。新生代对象通常生命周期较短,老生代对象生命周期较长。不同代使用不同的垃圾回收算法,提升效率。
增量垃圾回收(Incremental Garbage Collection)
将垃圾回收过程分成多个小步骤,避免一次性长时间暂停应用程序,提高应用程序的响应速度。
1.4.5 代码示例
标记和清除算法示例
let obj1 = { name: "Object 1" };
let obj2 = { name: "Object 2" };
obj1.linkedObj = obj2;
obj2.linkedObj = obj1;
// 手动断开引用,避免循环引用
obj1 = null;
obj2 = null;
// 垃圾回收器会在适当的时间回收obj1和obj2
引用计数示例
let objA = { name: "Object A" };
let objB = { name: "Object B" };
objA.ref = objB;
objB.ref = objA;
// 循环引用问题:即使设置为null,引用计数不为0,导致内存泄漏
objA = null;
objB = null;
// 垃圾回收器无法回收objA和objB,造成内存泄漏
分代垃圾回收示例
function createObjects() {
let newObj1 = { name: "New Object 1" };
let newObj2 = { name: "New Object 2" };
// 对象会被分配到新生代
return [newObj1, newObj2];
}
let objects = createObjects();
// 对象在短时间内可能会被回收
objects = null;
// 经过一定时间,对象会被移到老生代,进行更少的垃圾回收操作
1.4.6 总结
JavaScript的垃圾回收机制极大地简化了内存管理工作。标记和清除算法、引用计数和分代垃圾回收等策略共同作用,确保了内存的高效使用。理解这些机制有助于编写高效的代码,避免内存泄漏和性能问题。
1.5 闭包
闭包是指在函数内部定义的函数可以访问其外部函数的变量,即使外部函数已经执行完毕。
function outerFunction() {
var outerVar = "outer";
function innerFunction() {
console.log(outerVar);
}
return innerFunction;
}
var myClosure = outerFunction();
myClosure(); // "outer"
总结:
1.怎么理解闭包?
- 闭包 = 内层函数 + 外层函数的变量
2.闭包的作用?
- 封闭数据,实现数据私有,外部也可以访问函数内部的变量
- 闭包很有用,因为它允许将函数与其所操作的某些数据(环境)关联起来
3.闭包可能引起的问题?
- 内存泄漏
1.6 变量提升
变量提升是指变量声明会被提升到其所在作用域的顶部。需要注意的是,变量初始化不会被提升。
console.log(a); // undefined
var a = 5;
console.log(a); // 5
function hoistExample() {
console.log(b); // undefined
var b = 10;
console.log(b); // 10
}
hoistExample();
总结
理解作用域、作用域链和闭包是掌握 JavaScript 的关键。变量提升是 JavaScript 的特性之一,需要注意它对代码行为的影响。
2. 函数
JavaScript 函数是实现代码复用的重要手段。
2.1 函数提升
函数声明会被提升到作用域的顶部,这意味着可以在函数声明之前调用它。
sayHello();
function sayHello() {
console.log("Hello!");
}
2.2 函数参数
JavaScript 函数可以接受参数,参数可以有默认值,也可以通过动态参数和剩余参数实现灵活调用。
2.2.1 默认值
函数参数可以有默认值,当没有提供对应参数时会使用默认值。
function greet(name = "Guest") {
console.log(`Hello, ${name}!`);
}
greet(); // Hello, Guest!
greet("John"); // Hello, John!
2.2.2 动态参数
通过 arguments
对象可以访问函数的所有传入参数。
function sum() {
let total = 0;
for (let i = 0; i < arguments.length; i++) {
total += arguments[i];
}
return total;
}
console.log(sum(1, 2, 3)); // 6
2.2.3 剩余参数
剩余参数语法允许我们将不定数量的参数表示为一个数组。
function sum(...numbers) {
return numbers.reduce((total, num) => total + num, 0);
}
console.log(sum(1, 2, 3)); // 6
作为前端老师,我会详细介绍JavaScript中的展开运算符(Spread Operator)的主要知识点,并提供相应的代码示例来帮助理解。
2.2.4展开运算符
展开运算符使用三个点 (...
) 表示,可以用于对象和数组。它主要用于展开可迭代对象(如数组、字符串)或对象的属性。
1.在数组中的应用
(1)创建新数组
展开运算符可以将一个数组的元素展开到另一个数组中,创建一个新数组。
const arr1 = [1, 2, 3];
const arr2 = [...arr1, 4, 5, 6];
console.log(arr2); // 输出: [1, 2, 3, 4, 5, 6]
(2)数组合并
可以用展开运算符合并多个数组。
const arr1 = [1, 2];
const arr2 = [3, 4];
const arr3 = [5, 6];
const combinedArr = [...arr1, ...arr2, ...arr3];
console.log(combinedArr); // 输出: [1, 2, 3, 4, 5, 6]
(3)拷贝数组
展开运算符可以用于浅拷贝数组。
const originalArr = [1, 2, 3];
const copiedArr = [...originalArr];
console.log(copiedArr); // 输出: [1, 2, 3]
console.log(copiedArr === originalArr); // 输出: false
2. 在对象中的应用
(1)创建新对象
展开运算符可以将一个对象的属性展开到另一个对象中,创建一个新对象。
const obj1 = { a: 1, b: 2 };
const obj2 = { ...obj1, c: 3 };
console.log(obj2); // 输出: { a: 1, b: 2, c: 3 }
(2)对象合并
可以用展开运算符合并多个对象。
const obj1 = { a: 1 };
const obj2 = { b: 2 };
const obj3 = { c: 3 };
const combinedObj = { ...obj1, ...obj2, ...obj3 };
console.log(combinedObj); // 输出: { a: 1, b: 2, c: 3 }
(3)对象浅拷贝
展开运算符可以用于浅拷贝对象。
const originalObj = { a: 1, b: 2 };
const copiedObj = { ...originalObj };
console.log(copiedObj); // 输出: { a: 1, b: 2 }
console.log(copiedObj === originalObj); // 输出: false
3. 在函数调用中的应用
展开运算符可以将数组元素作为函数参数传递。
function sum(x, y, z) {
return x + y + z;
}
const numbers = [1, 2, 3];
console.log(sum(...numbers)); // 输出: 6
4. 在字符串中的应用
展开运算符可以将字符串展开成字符数组。
const str = "hello";
const chars = [...str];
console.log(chars); // 输出: ['h', 'e', 'l', 'l', 'o']
5. 代码案例
案例1:数组去重
使用展开运算符和 Set
来去除数组中的重复元素。
const numbers = [1, 2, 2, 3, 4, 4, 5];
const uniqueNumbers = [...new Set(numbers)];
console.log(uniqueNumbers); // 输出: [1, 2, 3, 4, 5]
案例2:合并多个对象并添加新属性
合并多个对象并添加新属性,展示对象合并和新属性添加。
const user = { name: 'Alice', age: 25 };
const job = { title: 'Developer', company: 'Tech Co.' };
const additionalInfo = { location: 'New York', active: true };
const userProfile = { ...user, ...job, ...additionalInfo, isAdmin: false };
console.log(userProfile);
// 输出: { name: 'Alice', age: 25, title: 'Developer', company: 'Tech Co.', location: 'New York', active: true, isAdmin: false }
案例3:复制并修改数组
复制一个数组并添加新元素,同时不修改原数组。
const originalArr = [1, 2, 3];
const modifiedArr = [...originalArr, 4, 5];
console.log(originalArr); // 输出: [1, 2, 3]
console.log(modifiedArr); // 输出: [1, 2, 3, 4, 5]
2.3 箭头函数
箭头函数是 ES6 引入的一种更简洁的函数语法。
2.3.1 箭头函数参数
箭头函数的参数和传统函数类似,但语法更简洁。
const add = (a, b) => a + b;
console.log(add(2, 3)); // 5
2.3.2 箭头函数 this
箭头函数没有自己的 this
,它会捕获其所在上下文的 this
值。
function Person() {
this.age = 0;
setInterval(() => {
this.age++;
console.log(this.age);
}, 1000);
}
let p = new Person();
// 输出 1, 2, 3, ...
总结
掌握函数提升、函数参数(包括默认值、动态参数和剩余参数)、箭头函数及其 this
绑定特性,可以提升代码的简洁性和可维护性。
3. 解构赋值
解构赋值是 ES6 引入的语法,可以从数组或对象中提取值并赋值给变量。
3.1 数组解构
数组解构允许我们从数组中提取值,并将其赋值给变量。
let [a, b] = [1, 2];
console.log(a); // 1
console.log(b); // 2
let [x, , y] = [1, 2, 3];
console.log(x); // 1
console.log(y); // 3
3.2 对象解构
对象解构允许我们从对象中提取属性,并将其赋值给变量。
let { name, age } = { name: "John", age: 30 };
console.log(name); // John
console.log(age); // 30
let { name: n, age: a } = { name: "John", age: 30 };
console.log(n); // John
console.log(a); // 30
总结
解构赋值使得从数组和对象中提取数据更加简洁和直观,有助于提高代码的可读性和简洁性。
4. 综合案例
通过综合使用以上知识点,我们可以编写更简洁、可维护性更高的代码。
4.1 forEach 遍历数组
forEach
方法用于遍历数组中的每个元素。
const numbers = [1, 2, 3, 4, 5];
numbers.forEach(num => {
console.log(num);
});
// 输出 1, 2, 3, 4, 5
4.2 filter 筛选数组
filter
方法创建一个新数组,其包含通过提供函数实现的测试的所有元素。
const numbers = [1, 2, 3, 4, 5];
const evenNumbers = numbers.filter(num => num % 2 === 0);
console.log(evenNumbers); // [2, 4]
综合案例:验证注册页面表单
<!DOCTYPE html>
<html>
<head>
<title>Registration Form</title>
</head>
<body>
<form id="registrationForm">
<label for="username">Username:</label>
<input type="text" id="username" name="username"><br>
<label for="email">Email:</label>
<input type="email" id="email" name="email"><br>
<label for="password">Password:</label>
<input type="password" id="password" name="password"><br>
<button type="submit">Register</button>
</form>
<script>
document.getElementById('registrationForm').addEventListener('submit', function(event) {
event.preventDefault();
const username = document.getElementById('username').value;
const email = document.getElementById('email').value;
const password = document.getElementById('password').value;
// 验证用户名
const usernameRegex = /^[a-zA-Z0-9_]{3,10}$/;
if (!usernameRegex.test(username)) {
console.log("Invalid username");
return;
}
// 验证邮箱
const emailRegex = /^[
^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
console.log("Invalid email");
return;
}
// 验证密码
const passwordRegex = /^[a-zA-Z0-9]{6,20}$/;
if (!passwordRegex.test(password)) {
console.log("Invalid password");
return;
}
console.log("Registration successful");
});
</script>
</body>
</html>
总结
通过以上内容的学习,理解并使用 JavaScript 中的作用域、函数、解构赋值等高级特性。继续加油!