前言
学完 HTML、CSS、JS 基础与 DOM/BOM 之后,JS 进阶是前端分水岭,是原生 JS 内核,也是 Vue、React 等框架底层原理。吃透本篇内容,才算真正理解 JavaScript 运行机制,解决工作中 90% 疑难 BUG,面试高频考点全部覆盖。
一、作用域与作用域链
1.1 什么是作用
作用域:限定变量的可访问范围,用来隔离变量,防止全局变量污染,不同作用域同名变量互不干扰。 JS 作用分为:全局作用域、局部作用(函数作用域 + 块级作用域)。
1.1.1 全局作用域
所有独立.js文件、<script>标签最外层代码属于全局作用域。
- 全局变量:全局任意作用域都能访问;
- 生命周期:页面打开创建,页面关闭销毁。
// 全局变量 let globalNum = 20; function test() { console.log(globalNum); // 函数内部可访问全局变量 } test();❌ 不规范写法:函数内不写 let/var 直接赋值,变量自动变为全局变量,项目严禁使用。
1.1.2 局部作用域
分为函数作用域、块级作用域({})
① 函数作用域
在function(){}内部,用 let/const/var 定义的变量仅在函数内生效,函数执行完毕,局部变量被 GC 回收。
- 函数形参也是局部变量;
- 不同函数内部变量互相无法访问。
function fn() { let num = 10; // 局部变量 } console.log(num); // 报错,外部无法访问② 块级作用域({}、for/if)
let、const声明变量会产生块作用域,var 没有块作用域。
for(let i = 1; i <= 5; i++) { console.log(i); } console.log(i); // 报错,i仅限循环内部1.2 作用域链
1.2 原理
多层嵌套函数,层层向外形成链式结构;查找变量规则:先在当前作用域查找→找不到逐层向上找父级→直到全局,找不到报错。
- 子作用域可以访问父作用域变量;父不能访问子内部变量。
let a = 10; // 全局 function outer() { let b = 20; // 外层局部 function inner() { let c = 30; console.log(a, b, c); // 逐层向上查找 } inner(); } outer();1.3 垃圾回收机制 GC
JS 自动管理内存,不需要程序员手动开辟 / 释放内存。
1.3.1 内存生命周期
- 内存分配:定义变量、对象、函数,JS 自动分配堆 / 栈内存;
- 内存使用:读写变量、调用函数;
- 内存回收:数据不再使用,GC 自动回收释放内存。
全局变量:页面关闭才回收;局部变量:函数执行结束自动回收。 内存泄漏:内存已经无用,但无法被 GC 回收,长期占用内存。
1.3.2 两种主流 GC 算法
- 引用计数法(老 IE 专用)规则:统计对象被引用次数,次数 = 0 就回收; 缺陷:循环引用无法回收,造成内存泄漏
let o1 = {}, o2 = {}; o1.a = o2; o2.a = o1; // 互相引用,计数永远不为0,无法释放- 标记清除(现代 Chrome/Firefox 主流)从全局 window 作为根节点遍历,能访问到标记存活,无法访问直接回收,完美解决循环引用问题。
二、闭包(面试超级重点)
2.1 闭包定义
闭包 = 内层函数 + 外层函数的局部变量;内层函数引用外层函数变量,外层函数执行完毕,局部变量不会被 GC 销毁。
function outer() { let i = 1; // 外层局部变量 function inner() { console.log(i); } return inner; // 返回内层函数 } const fn = outer(); fn(); // 外部调用,访问外层变量2.2 闭包作用
- 私有化变量,变量无法从全局随意修改;
- 延长局部变量生命周期;
弊端:滥用闭包容易内存占用过高,造成内存泄漏。
三、变量提升与函数提升
3.1 变量提升(仅 var 有效)
- var:声明提升到当前作用域顶部,先使用后定义值为
undefined; - let/const:不存在变量提升,暂时性死区,先使用直接报错。
console.log(str); // undefined var str = "js进阶"; // let str; 提前访问直接报错3.2 函数提升
- 函数声明 function fn (){}:整体提升,调用可以写在定义前面
fn(); function fn() {}- 函数表达式 const fn=function (){}:无提升,必须先定义再调用
fn(); // 报错 const fn = function(){}四、函数进阶用法
4.1 arguments 动态参数
函数内置伪数组,存储所有实参,不确定参数个数时用来求和,只有普通函数拥有,箭头没有。
function sum() { let total = 0; for(let i = 0; i < arguments.length; i++) { total += arguments[i]; } return total; } sum(10,20,30);4.2 剩余参数 ...args
形参末尾书写...,接收多余实参,返回真数组,开发优先替代 arguments。
function test(base, ...other) { console(base, other) } test("域名", "get", "json")4.3 展开运算符 ...
作用:展开数组 / 对象,常用:数组拼接、Math 最大值
let arr = [1,5,9]; console.log(Math.max(...arr)); let newArr = [...arr, 88];4.4 箭头函数 ES6(=>)
简写规则
- 无参数:
()=>{} - 单个参数可省略括号:
x=>{} - 单行代码省略 {} 与 return:
(a,b)=>a+b - 返回对象必须外层加括号:
n=>({name:n})
const add = (a,b)=>a+b;箭头函数四大特性
- 没有 arguments,用剩余参数...args 替代
- 不绑定 this,继承上层作用域 this(DOM 绑定事件慎用,this 指向 window)
- 不能用作构造函数 new
- 不能使用 yield
五、解构赋值 ES6
5.1 数组解构
快速批量把数组值赋值给变量
//基础解构 const [a,b,c] = [10,20,30]; //剩余参数接收后面所有元素 const [x,...rest] = ["小米","华为","苹果"]; //默认值,只有对应值为undefined生效 const [m=5] = []; //变量交换 let n1=1,n2=2;[n1,n2]=[n2,n1]; //跳过元素 const [a,,c] = [1,2,3];5.2 对象解构
const user = {name:"张三",age:18}; const {name,age} = user; //属性重命名 const {name:uname} = user;六、数组高阶 API(项目高频)
6.1 forEach () 遍历数组
无返回值,单纯循环遍历每个元素
const arr = ["红","绿"]; arr.forEach((item,index)=>{ console.log(index,item); })6.2 filter () 筛选数组
返回满足条件新数组,原数组不变
let arr = [10,50,33]; let res = arr.filter(item=>item>30);6.3 reduce 累加器
多用于数组求和,参数:prev 上次结果、curr 当前值
let arr = [1,2,3,4]; let sum = arr.reduce((prev,curr)=>prev+curr,0);6.4 其他常用数组方法
| 方法 | 功能 |
|---|---|
| find | 返回第一个符合条件元素 |
| every | 全部满足返回 true |
| some | 任意一个满足返回 true |
| concat | 数组合并 |
| join | 数组转字符串 |
| splice | 删除 / 替换元素 |
| Array.from | 伪数组转真数组 |
七、字符串常用 API
split('分隔符'):字符串切割成数组substring(起始,结束):字符串截取includes():判断是否包含字符toUpperCase/toLowerCase:大小写转换replace(正则/字符,替换内容):字符替换
八、面向对象 OOP
8.1 两种编程思想
- 面向过程 POP:分步拆解,按步骤执行(例:做蛋炒饭:备菜→炒饭→出锅),注重过程;
- 面向对象 OOP:拆分对象,对象分工协作(盖浇饭:米饭 + 配菜),注重事物,三大特性:封装、继承、多态。
8.2 创建对象三种写法
- 对象字面量
const obj={name:""} - new Object()
- 构造函数(批量创建同类对象)
8.3 构造函数
规则:函数名首字母大写,new 关键字实例化对象
function Pig(name,age) { this.name = name; this.age = age; } const peppa = new Pig("佩奇",6);new 执行四步
- 在内存开辟空对象;
- 绑定构造函数 this 指向新对象;
- 执行构造函数内代码,给对象添加属性;
- 自动返回新对象(无需 return)。
实例成员 & 静态成员
- 实例成员:
this.xxx,实例对象访问; - 静态成员:
构造函数.属性,只能构造函数访问,实例无法访问
Pig.eye = 2; //静态属性8.4 原型 prototype(节省内存核心)
1. 原型对象
每个构造函数自带prototype原型对象,存放所有实例共用的方法,所有实例共享,节省内存
function Star(name) { this.name = name; } // 原型挂载公共方法 Star.prototype.sing = function(){} let s1 = new Star("刘德华"); let s2 = new Star("张学友"); console.log(s1.sing === s2.sing) // true 共用同一个函数2.proto对象原型
所有实例自带__proto__属性,指向构造函数的 prototype 原型,实例能调用原型方法全靠它。
3. constructor 属性
原型默认constructor指向原构造函数;直接重写原型对象会丢失 constructor,需要手动修正
Star.prototype = { constructor:Star, sing:function(){} }8.5 原型链
实例.proto→ 构造函数.prototype → 原型.proto→ Object.prototype → null
属性查找规则
- 先在自身找属性;
- 找不到去
__proto原型找; - 逐级向上找到 Object 原型,无则 undefined;
instanceof:检测构造函数是否出现在实例原型链上。
九、深浅拷贝(只针对引用数据类型)
基本数据:赋值直接复制值;引用数据:默认赋值复制内存地址。
9.1 浅拷贝
仅拷贝对象第一层,嵌套对象拷贝地址,修改嵌套内容原对象同步变化常用方法:
- 对象:
{...obj}、Object.assign({},obj) - 数组:
[...arr]、concat()
let obj = {a:1,info:{b:2}} let newObj = {...obj}; newObj.info.b = 99; //原对象info同步改变9.2 深拷贝
完整开辟新内存,新老对象完全隔离,修改互不影响 三种实现:
JSON.parse(JSON.stringify(obj))(无法处理函数、undefined)- 递归手写深拷贝
- 第三方库 lodash:
_.cloneDeep(obj)
十、this 指向 (重中之重)
10.1 this 分三类
- 普通函数
- 直接调用:this = window(严格模式 undefined)
- 对象。方法调用:this = 当前对象
- 箭头函数:无自身 this,继承外层上下文 this
- 构造函数:this = new 出来的实例
10.2 三个修改 this 方法
| 方法 | 执行特性 | 传参形式 |
| call | 立即执行函数 | 逐个传参 fn.call (obj,1,2) |
| apply | 立即执行函数 | 数组传参 fn.apply (obj,[1,2]) |
| bind | 不执行,返回新函数永久绑定 this | 逐个传参 |
function test(a,b){console(this,a+b)} test.call({name:"小明"},1,2) test.apply({name:"小明"},[1,2]) let fn = test.bind({name:"小明"},1,2) fn();十一、异常处理
11.1 throw 主动抛出错误
throw new Error("参数非法"),抛出后终止后续代码
11.2 try/catch 捕获运行错误
try { // 可能报错代码 let a = undefined + 1; } catch(err) { // 捕获错误信息 console.log(err.message) }十二、学习寄语
JS 进阶是原生 JS 的分水岭,原型、闭包、this、作用域都是抽象概念,不要死记,多敲代码、打印测试、手绘原型链。坚持每天练习案例,吃透本章节,后续 Vue、React 学习效率翻倍,前端之路稳步进阶,日积月累,你一定可以成为资深前端工程师!