文章目录
- 一、作用域与作用域链
- 二、内存管理与闭包
- 三、变量提升与函数提升
-
- [3.1 变量提升](#3.1 变量提升)
- [3.2 函数提升](#3.2 函数提升)
- 四、函数进阶
- 五、解构赋值
-
- [5.1 数组解构](#5.1 数组解构)
- [5.2 数组解构细节](#5.2 数组解构细节)
- [5.3 对象解构](#5.3 对象解构)
- [5.4 多级对象解构](#5.4 多级对象解构)
- [5.5 多级对象解构案例](#5.5 多级对象解构案例)
- 六、数组高阶方法
-
- [6.1 forEach - 遍历数组](#6.1 forEach - 遍历数组)
- [6.2 filter - 筛选数组](#6.2 filter - 筛选数组)
- [6.4 方法对比](#6.4 方法对比)
- 七、最佳实践与注意事项
-
- [7.1 分号使用规则](#7.1 分号使用规则)
- [7.2 作用域最佳实践](#7.2 作用域最佳实践)
- [7.3 闭包使用建议](#7.3 闭包使用建议)
- 总结
一、作用域与作用域链
1.1 作用域的类型
局部作用域
- 函数作用域:在函数内部声明的变量只能在函数内部访问
javascript
function calculate(x, y) {
const result = x + y; // 函数作用域内的变量
console.log(result);
return result;
}
calculate(10, 5); // 15
// console.log(result); // 报错:result未定义
- 块作用域 :由
{}代码块创建的作用域
javascript
{
let blockScoped = "我在块作用域内";
const constantValue = "我也是";
console.log(blockScoped); // 正常访问
}
// console.log(blockScoped); // 报错
// console.log(constantValue); // 报错
// var 没有块作用域
if (true) {
var globalLike = "我实际上在全局作用域";
let trulyBlocked = "我确实在块作用域";
}
console.log(globalLike); // 正常访问
// console.log(trulyBlocked); // 报错
总结:
let声明的变量会产生块作用域,var不会产生块作用域const声明的常量也会产生块作用域- 不同代码块之间的变量无法互相访问
- 推荐使用
let或const
注:开发中let和const经常不加区分的使用,如果担心某个值会不小被修改时,则只能使用const声明成常量。
全局作用域
最外层的作用域,任何地方都可以访问
javascript
const globalVar = "我是全局变量";
function accessGlobal() {
console.log(globalVar); // 可以访问
innerGlobal = "危险!隐式全局变量"; // 没有声明关键字,成为全局变量
}
accessGlobal();
console.log(innerGlobal); // 可以访问,但不推荐这种写法
总结:
- 为
window对象动态添加的属性默认也是全局的,不推荐!- 函数中未使用任何关键字声明的变量为全局变量,不推荐!!!
- 尽可能少的声明全局变量,防止全局变量被污染
1.2 作用域链与变量查找
作用域链本质上是JavaScript底层的变量查找机制:在函数被执行时,会优先查找当前函数作用域中查找变量,如果当前作用域查找不到则会依次逐级查找父级作用域直到全局作用域
javascript
let outer = "全局变量";
function outerFunction() {
let middle = "外层函数变量";
function innerFunction() {
let inner = "内层函数变量";
console.log(inner); // 1. 优先查找当前作用域
console.log(middle); // 2. 查找父级作用域
console.log(outer); // 3. 查找全局作用域
// console.log(notExist); // 报错:所有作用域都找不到
}
innerFunction();
// console.log(inner); // 报错:父作用域不能访问子作用域
}
outerFunction();
总结:
- 嵌套关系的作用域串联起来形成了作用域链
- 相同作用域链中按着从小到大的规则查找变量
- 子作用域能够访问父作用域,父级作用域无法访问子级作用域
二、内存管理与闭包
2.1 JavaScript垃圾回收机制
内存生命周期
- 内存分配:声明变量、函数、对象时自动分配
- 内存使用:读写内存,使用变量、函数等
- 内存回收:不再使用的内存由垃圾回收器自动回收
说明:
- 全局变量一般不会回收(关闭页面回收)
- 一般情况下局部变量的值,不用了,会被自动回收掉
内存泄漏:程序中分配的内存由于某种原因程序未释放或无法释放叫做内存泄漏
堆栈空间分配
- 栈(操作系统):由操作系统自动分配释放函数的参数值、局部变量等,
基本数据类型放到栈里面 - 堆(操作系统):一般由程序员分配释放,若程序员不释放,由垃圾回收机制回收,
复杂数据类型放到堆里面
javascript
// 栈内存 - 存储基本类型和引用地址
let num = 10; // 栈内存
let str = "hello"; // 栈内存
// 堆内存 - 存储复杂类型
let obj = { // obj在栈,{name: "John"}在堆
name: "John",
age: 30
};
let arr = [1, 2, 3]; // arr在栈,[1,2,3]在堆
垃圾回收算法
引用计数法(已逐渐淘汰)
跟踪每个值被引用的次数,引用次数为0,则清除
javascript
// 嵌套引用问题
function problem() {
let objA = {};
let objB = {};
objA.ref = objB; // objB引用次数: 1
objB.ref = objA; // objA引用次数: 1
// 即使离开作用域,引用计数不为0,无法回收
}
标记清除法(现代浏览器主要使用)
从根对象(全局对象)开始标记所有可达对象,清除未被标记的对象
2.2 闭包
闭包的基本概念
闭包 = 内层函数 + 外层函数的变量
javascript
// 基本闭包格式
function outer() {
let a = 100
function fn() {
console.log(a)
}
return fn
}
const fun = outer()
fun()
闭包的实际应用
javascript
// 1. 数据私有化
// 统计函数调用次数
function count() {
let i = 0;
function fn() {
i++;
console.log(`函数被调用了${i}次`);
}
return fn;
}
const fun = count();
fun(); // 函数被调用了1次
fun(); // 函数被调用了2次
闭包的内存泄漏问题
javascript
// 可能造成内存泄漏的示例
function leakyFunction() {
const largeData = new Array(1000000).fill("data");
return function() {
console.log("我持有largeData的引用");
// 即使largeData不再需要,由于闭包引用,无法被回收
};
}
// 解决方法:及时释放引用
function safeFunction() {
const largeData = new Array(1000000).fill("data");
const innerFunction = function() {
console.log("使用数据");
};
// 使用后手动解除引用
largeData = null;
return innerFunction;
}
总结:
- 闭包 = 内层函数 + 外层函数的变量
- 闭包的作用:封闭数据,实现数据私有,外部也可以访问函数内部的变量
- 闭包可能会造成内存泄漏
三、变量提升与函数提升
3.1 变量提升
javascript
// 变量提升示例
// 把所有var声明的变量提升到当前作用域的最前面
// 只提升声明,不提升赋值
function fn() {
console.log(num); // undefined
var num = 10;
}
// 相当于
function fn() {
var num;
console.log(num);
num = 10;
}
总结:
- 变量在未声明即被访问时会报语法错误
- 变量在声明之前即被访问,变量的值为
undefinedlet声明的变量不存在变量提升,推荐使用let- 变量提升出现在相同作用域当中
- 实际开发中推荐先声明再访问变量
3.2 函数提升
javascript
// 函数声明会提升
// 会把所有函数声明提升到当前作用域的最前面
// 只提升函数声明,不提升函数调用
fn(); // 函数提升
function fn() {
console.log('函数提升');
}
// 函数表达式不会提升
fun(); // 错误
var fun = function() {
console.log('函数表达式');
};
总结:
- 函数提升能够使函数的声明调用更灵活
- 函数表达式不存在提升的现象
- 函数提升出现在相同作用域当中
四、函数进阶
4.1 函数参数
默认参数
javascript
<script>
// 设置参数默认值
function sayHi(name="小明", age=18) {
document.write(`<p>大家好,我叫${name},我今年${age}岁了。</p>`);
}
// 调用函数
sayHi();
sayHi('小红');
sayHi('小刚', 21);
</script>
动态参数
arguments是函数内部的伪数组,包含所有传递给函数的参数。
javascript
function getSum() {
let sum = 0;
for (let i = 0; i < arguments.length; i++) {
sum += arguments[i];
}
console.log(sum);
}
getSum(2, 3, 4); // 9
总结:
arguments是一个伪数组arguments的作用是动态获取函数的实参
剩余参数
javascript
function getSum(a, b, ...arr) {
let sum = 0;
for (let i = 0; i < arr.length; i++) {
sum += arr[i];
}
console.log(sum);
}
getSum(1, 2, 3, 4, 5); // 12
总结:
...是语法符号(展开运算符),置于最末函数形参之前,用于获取多余的实参- 借助
...获取的剩余实参,是个真数组
展开运算符
展开数组,将数组转换为逗号分隔的参数序列,不会修改原数组,应用于:求数组最大(小)值,合并数组等
javascript
// 展开数组
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
// 1. 求最大(小)值
console.log(Math.max(...arr1)); // 3
console.log(Math.min(...arr1)); // 1
// 2. 合并数组
const arr = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6]
// 复制数组(浅拷贝)
const arrCopy = [...arr1];
// 展开对象
const obj1 = { a: 1, b: 2 };
const obj2 = { ...obj1, c: 3 }; // {a:1, b:2, c:3}
4.2 箭头函数
基本语法
javascript
// 基本语法
const fn1 = () => {
console.log(123);
};
// 有参数的箭头函数(单个参数可省略括号)
const fn2 = x => {
console.log(x);
};
// 只有一行代码时,可省略大括号和return
const fn3 = x => x + x;
// 直接返回对象(需要用括号包裹)
const fn4 = (uname) => ({ uname: uname });
总结:
- 箭头函数属于表达式函数,因此不存在函数提升
- 箭头函数只有一个参数时可以省略圆括号
()- 箭头函数函数体只有一行代码时可以省略花括号
{},并自动做为返回值被返回
this 指向
箭头函数的this指向是固定的,指向外层作用域的this
javascript
<script>
// 以前this的指向: 谁调用的这个函数,this 就指向谁
// console.log(this) // window
// // 普通函数
// function fn() {
// console.log(this) // window
// }
// window.fn()
// // 对象方法里面的this
// const obj = {
// name: 'andy',
// sayHi: function () {
// console.log(this) // obj
// }
// }
// obj.sayHi()
// 2. 箭头函数的this 是上一层作用域的this 指向
// const fn = () => {
// console.log(this) // window
// }
// fn()
// 对象方法箭头函数 this
// const obj = {
// uname: 'pink老师',
// sayHi: () => {
// console.log(this) // this 指向谁? window
// }
// }
// obj.sayHi()
const obj = {
uname: 'pink老师',
sayHi: function () {
console.log(this) // obj
let i = 10
const count = () => {
console.log(this) // obj
}
count()
}
}
obj.sayHi()
</script>
箭头函数参数
箭头函数中没有 arguments,只能使用 ... 动态获取实参
javascript
const getSum = (...arr) => {
let sum = 0;
for (let i = 0; i < arr.length; i++) {
sum += arr[i];
}
return sum;
};
console.log(getSum(2, 3, 5)); // 10
五、解构赋值
5.1 数组解构
数组解构是将数组的单元值快速批量赋值给一系列变量的简洁语法
javascript
// 基本用法
const [max, min, avg] = [100, 60, 80];
console.log(max); // 100
// 变量交换
let a = 1, b = 2;
[b, a] = [a, b];
console.log(a, b); // 2 1
5.2 数组解构细节
javascript
// 1. 变量多,单元值少(未匹配的变量为undefined)
const [a, b, c, d] = [1, 2, 3];
console.log(d); // undefined
// 2. 变量少,单元值多(忽略多余的值)
const [x, y] = [1, 2, 3];
// 3. 使用剩余参数收集多余的值
const [i, j, ...k] = [1, 2, 3, 4];
console.log(k); // [3, 4]
// 4. 设置默认值
const [o = 0, p = 0] = [];
console.log(o, p); // 0 0
// 5. 跳过某些值
const [e, f, , h] = [1, 2, 3, 4];
console.log(e, f, h); // 1 2 4
// 6. 多维数组解构
const [l, m, [n, g]] = [1, 2, [3, 4]];
5.3 对象解构
javascript
// 基本用法
const { uname, age } = { uname: 'pink老师', age: 18 };
// 变量重命名
const { uname: username, age } = { uname: 'pink老师', age: 18 };
// 解构数组中的对象
const pig = [
{
name: '佩奇',
age: 6
}
];
const [{ name: uname, age }] = pig;
5.4 多级对象解构
javascript
// 多级数组对象解构
const pig = [
{
name: '佩奇',
family: {
mother: '猪妈妈',
father: '猪爸爸',
sister: '乔治'
},
age: 6
}
];
const [{ name, family: { mother, father, sister }, age }] = pig;
5.5 多级对象解构案例
javascript
<body>
<script>
// 1. 这是后台传递过来的数据
const msg = {
"code": 200,
"msg": "获取新闻列表成功",
"data": [
{
"id": 1,
"title": "5G商用自己,三大运用商收入下降",
"count": 58
},
{
"id": 2,
"title": "国际媒体头条速览",
"count": 56
},
{
"id": 3,
"title": "乌克兰和俄罗斯持续冲突",
"count": 1669
},
]
}
// 需求1: 请将以上msg对象 采用对象解构的方式 只选出 data 方面后面使用渲染页面
// const { data } = msg
// console.log(data)
// 需求2: 上面msg是后台传递过来的数据,我们需要把data选出当做参数传递给 函数
// const { data } = msg
// msg 虽然很多属性,但是我们利用解构只要 data值
function render({ data }) {
// const { data } = arr
// 我们只要 data 数据
// 内部处理
console.log(data)
}
render(msg)
// 需求3, 为了防止msg里面的data名字混淆,要求渲染函数里面的数据名改为 myData
function render({ data: myData }) {
// 要求将 获取过来的 data数据 更名为 myData
// 内部处理
console.log(myData)
}
render(msg)
</script>
六、数组高阶方法
6.1 forEach - 遍历数组
forEach是数组的遍历方法,适用于遍历数组对象
javascript
const arr = ['red', 'green', 'pink'];
arr.forEach(function(item, index) {
console.log(item); // 元素值
console.log(index); // 索引
});
// 使用箭头函数简写
arr.forEach((item, index) => {
console.log(item, index);
});
6.2 filter - 筛选数组
filter() 方法创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素
javascript
const arr = [10, 20, 30];
// 使用函数表达式
const newArr1 = arr.filter(function(item) {
return item >= 20;
});
// 使用箭头函数简写
const newArr2 = arr.filter(item => item >= 20);
console.log(newArr2); // [20, 30]
6.4 方法对比
| 方法 | 返回值 | 是否改变原数组 | 用途 |
|---|---|---|---|
| forEach | undefined | 否 | 遍历数组,执行副作用 |
| filter | 新数组 | 否 | 筛选满足条件的元素 |
| map | 新数组 | 否 | 转换数组元素 |
| find | 第一个匹配元素 | 否 | 查找单个元素 |
| reduce | 累计值 | 否 | 累加或转换数组 |
七、最佳实践与注意事项
7.1 分号使用规则
javascript
// 必须加分号的情况
// 1. 立即执行函数前
(function() {
console.log("IIFE");
})();
// 安全写法:前面加分号
;(function() {
console.log("安全的IIFE");
})();
// 2. 以数组开头的代码前
const name = "JavaScript"
;[1, 2, 3].forEach(n => console.log(n))
// 3. 以模板字符串开头的代码前
const greeting = "Hello"
`world`.toUpperCase() // 可能出错
7.2 作用域最佳实践
javascript
// 1. 优先使用const,其次是let
const PI = 3.14159;
let counter = 0;
// 2. 避免使用var
// ❌ 不好
var oldVar = "过时";
// ✅ 好
let modernLet = "现代";
const modernConst = "更现代";
// 3. 立即执行函数创建私有作用域
(function() {
const privateVar = "外部无法访问";
// 模块代码
})();
// 4. 模块化组织代码
const Module = (function() {
let privateData = "私有";
return {
publicMethod: function() {
return privateData;
}
};
})();
7.3 闭包使用建议
javascript
// 1. 明确闭包的目的
function createCache() {
const cache = new Map();
return {
set: (key, value) => cache.set(key, value),
get: (key) => cache.get(key),
has: (key) => cache.has(key)
};
}
// 2. 避免不必要的闭包
function avoidClosure(arr) {
// ❌ 不必要的闭包
arr.forEach(function(item) {
console.log(item);
});
// ✅ 更简洁
arr.forEach(item => console.log(item));
}
// 3. 注意内存管理
function createHeavyClosure() {
const largeData = new Array(1000000);
const useData = () => {
// 使用数据
};
// 使用完毕后解除引用
const cleanup = () => {
largeData.length = 0;
};
return { useData, cleanup };
}
总结
JavaScript的进阶特性提供了更强大、更灵活的编程能力。通过深入理解作用域、闭包、函数进阶特性等概念,可以编写出更简洁、更高效、更易维护的代码。关键是要理解这些特性背后的原理,而不是仅仅记住语法。在实际开发中,应该根据具体场景选择合适的技术方案,并遵循最佳实践,避免常见的陷阱和性能问题。