02 - JS 进阶
目录
一、作用域与闭包
作用域的分类
- 全局作用域
- 函数作用域
- 块级作用域(ES6,由
let/const创建)
在当前作用域中没有定义的变量称为自由变量,访问它时会不断向定义时的上级作用域寻找(词法作用域)。
闭包
定义:自由变量的查找,是在函数定义的地方向上级作用域寻找,与函数执行的地方无关。
两种典型形式:
函数作为参数被传递,在另一个地方执行:
jsfunction print(fn) { const a = 200; fn(); } const a = 100; function fn() { console.log(a); // 打印 100(在定义的地方找 a) } print(fn);函数作为返回值:
jsfunction create() { const a = 100; return function() { console.log(a); // 打印 100 }; } const fn = create(); const a = 200; fn(); // 100
闭包的实际应用:隐藏数据
js
// 利用闭包实现私有缓存工具
function createCache() {
const data = {}; // 不对外暴露
return {
set(key, val) { data[key] = val; },
get(key) { return data[key]; }
};
}
const c = createCache();
c.set('a', 100);
console.log(c.get('a')); // 100
经典闭包面试题:点击事件中的 i 值
js
// ❌ 写法一:var 声明,闭包共享变量
let i, a;
for (i = 0; i < 10; i++) {
a = document.createElement('a');
a.innerHTML = i + '<br/>';
a.addEventListener('click', function(e) {
e.preventDefault();
alert(i); // 点击后均弹出 10(闭包捕获的是同一个 i)
});
document.body.appendChild(a);
}
// ✅ 写法二:let 声明,每次循环有独立的块级作用域
for (let i = 0; i < 10; i++) {
a = document.createElement('a');
a.innerHTML = i + '<br/>';
a.addEventListener('click', function(e) {
e.preventDefault();
alert(i); // 点击后弹出对应的 0~9
});
document.body.appendChild(a);
}二、this 指向
this取什么值,是在函数执行时确认的,不是定义时确认的。
五种情况
| 调用方式 | this 指向 |
|---|---|
| 普通函数直接调用 | window(非严格模式) / undefined(严格模式) |
| 对象方法调用 | 调用该方法的对象 |
new 构造函数 | 新创建的实例对象 |
call/apply/bind | 指定的第一个参数 |
| 箭头函数 | 继承外层(定义时所在)作用域的 this |
普通函数与对象中的 this
js
let obj = {
id: 1,
myName() {
console.log(this); // { id: 1, myName: f }(obj 调用,this 是 obj)
},
wait() {
setTimeout(function() {
console.log(this); // window(setTimeout 里普通函数由浏览器调用)
});
},
waitAgain() {
setTimeout(() => {
console.log(this); // obj(箭头函数继承外层 this)
});
}
};箭头函数中的 this
js
var name = 'name 1';
const stu = {
name: 'name 2',
sayName1: () => {
console.log(this.name); // name 1(外层是全局,this 是 window)
},
sayName2() {
console.log(this.name); // name 2(obj 调用)
},
sayName3: function() {
return () => {
console.log(this.name); // name 2(箭头函数继承外层函数的 this)
};
},
sayName4: () => {
return function() {
console.log(this.name); // name 1(普通函数由调用者决定)
};
}
};

手写 bind
js
Function.prototype.myBind = function(context, ...args) {
const self = this;
const bindedFunc = function(...callArgs) {
// 如果通过 new 调用,this 指向新实例;否则指向 context
const newThis = (this instanceof bindedFunc) ? this : context;
return self.apply(newThis, args.concat(callArgs));
};
// 保留原型链
if (self.prototype) {
bindedFunc.prototype = Object.create(self.prototype);
}
return bindedFunc;
};
三、箭头函数
与普通函数的区别
| 特性 | 普通函数 | 箭头函数 |
|---|---|---|
this 绑定 | 由调用方式决定 | 继承外层作用域的 this |
arguments | 有 | 无(用 ...rest 代替) |
new 构造 | 支持 | ❌ 不支持(不能作为构造函数) |
yield | 支持 | ❌ 不支持(不能作为 Generator) |
call/apply/bind 改变 this | 支持 | ❌ 无效 |
什么时候不能用箭头函数
- ❌ 作为对象的方法(
this会指向外层,而非对象) - ❌ 作为构造函数(
new会报错) - ❌ 需要使用
arguments(箭头函数没有) - ❌ 事件监听中需要
this指向事件源时 - ❌ Vue 2 Options API / Vue 生命周期函数(Vue 组件本质是 JS 对象)
Vue 3 Composition API 中可以完全使用箭头函数。 React class 组件(本质是 ES6 class)中可以使用箭头函数定义方法。
四、Generator 生成器
js
// 定义生成器函数:function* 声明
function* myGenerator() {
yield 1; // 暂停,返回 1
yield 2; // 暂停,返回 2
return 3; // 结束,done: true
}
const gen = myGenerator();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: true }双向通信(yield 传值)
js
function* counter() {
const a = yield 'first';
console.log('接收到:', a);
const b = yield 'second';
console.log('接收到:', b);
return 'done';
}
const g = counter();
g.next(); // { value: 'first', done: false }
g.next(100); // 输出 '接收到: 100',{ value: 'second', done: false }
g.next(200); // 输出 '接收到: 200',{ value: 'done', done: true }五、for...in 与 for...of
| 特性 | for...in | for...of |
|---|---|---|
| 遍历内容 | key(键) | value(值) |
| 遍历对象 | ✅ 支持 | ❌ 不支持(对象不是可迭代的) |
| 遍历数组 | ✅(得到索引) | ✅(得到值) |
| 遍历 Map/Set | ❌ | ✅ |
| 遍历 Generator | ❌ | ✅ |
| 本质 | 可枚举(enumerable) | 可迭代(iterable,有 Symbol.iterator) |
js
// for...in 遍历对象
const obj = { a: 1, b: 2 };
for (let key in obj) {
console.log(key); // 'a', 'b'
}
// for...of 遍历数组
const arr = [10, 20, 30];
for (let val of arr) {
console.log(val); // 10, 20, 30
}表面上是
for...invsfor...of,本质上是可枚举与可迭代的区别。
六、函数柯里化
将接受多个参数的函数,转化为接受单个参数的一系列函数的技术。
js
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return (...args2) => curried.apply(this, args.concat(args2));
}
};
}
// 使用示例
const sum = (a, b, c) => a + b + c;
const curriedSum = curry(sum);
console.log(curriedSum(1)(2)(3)); // 6
console.log(curriedSum(1, 2)(3)); // 6
console.log(curriedSum(1)(2, 3)); // 6七、防抖与节流
防抖(Debounce)
定义:连续触发时,只有停止操作后等待指定时间才执行。适合搜索框输入。
js
function debounce(fn, delay = 200) {
let timer = 0;
return function(...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
timer = 0;
}, delay);
};
}
// 示例
const input = document.getElementById('input1');
input.addEventListener('keyup', debounce(() => {
console.log('搜索:' + input.value);
}, 300));闭包作用:
timer变量不会被销毁,始终在debounce作用域中,确保可以清除上一次的setTimeout。
节流(Throttle)
定义:高频触发时,按固定时间间隔执行。适合 scroll、drag 等高频事件。
js
function throttle(fn, delay = 100) {
let timer = 0;
return function(...args) {
if (timer) return;
timer = setTimeout(() => {
fn.apply(this, args);
timer = 0;
}, delay);
};
}
// 示例
const card = document.getElementById('card1');
card.addEventListener('drag', throttle((e) => {
console.log(`鼠标位置: ${e.offsetX}, ${e.offsetY}`);
}, 300));对比
| 防抖(Debounce) | 节流(Throttle) | |
|---|---|---|
| 触发时机 | 停止操作后延迟触发(只触发最后一次) | 按固定频率触发(从高频中择几次) |
| 适用场景 | 搜索框输入、表单提交 | scroll、drag、resize 事件 |
实际工作中可使用 lodash 的
_.debounce和_.throttle。
八、V8 内存管理与垃圾回收
分代垃圾回收策略
新生代(Young Generation)
- 存储新创建的对象(大多数对象短命)
- 使用 Scavenge(复制算法):
- 内存分为 from space 和 to space 两半
- GC 时将存活对象复制到 to space,其余丢弃
- 角色交换,暂停时间短(Minor GC)
- 经历多次 GC 仍存活的对象晋升到老生代
老生代(Old Generation)
- 存储长生命周期的对象
- 使用 标记-清除(Mark-Sweep) + 标记-整理(Mark-Compact):
- 标记所有可达对象
- 清除未标记对象
- 整理内存碎片(移动存活对象到一端)
垃圾回收触发条件
- 内存阈值:堆内存达到一定比例时触发
- 分配失败:新对象无法在当前空间分配时触发
- 隐式触发:事件循环空闲时自动运行
