1. JS 的原型
问题: 能讲讲 JavaScript 的原型机制吗?__proto__
和 prototype
有什么区别?
回答:
JavaScript 是基于原型的面向对象语言,没有传统类的概念(ES6 的 class 只是语法糖)。它的继承机制依赖于"原型链"。
每个函数都有一个 prototype
属性,它指向一个对象,这个对象就是该函数作为构造函数时,其实例的原型。而每个对象都有一个内部属性 [[Prototype]]
,在浏览器中通常暴露为 __proto__
,它指向其构造函数的 prototype
。
当访问一个对象的属性时,如果该对象本身没有,就会沿着 __proto__
向上查找,直到 null
,这就是原型链。
js
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function () {
console.log(`Hello, I'm ${this.name}`);
};
const p1 = new Person('Alice');
p1.sayHello(); // "Hello, I'm Alice"
原型链查找过程
图解:
- A → B :尝试调用
p1.sayHello()
,JS 引擎首先检查p1
自身是否有该属性。 - B → C → D :没有,于是通过
__proto__
指向其构造函数Person
的prototype
对象。 - D → E → G :在
Person.prototype
上找到了sayHello
方法,绑定this
为p1
并执行。
关键点:
prototype
是函数才有的属性,用于实例继承。__proto__
是对象的内部原型引用(现代应使用Object.getPrototypeOf()
)。- 所有对象最终原型链都会指向
Object.prototype
,其__proto__
为null
。
2. 变量作用域链
问题: 什么是作用域链?它在变量查找中起什么作用?
回答:
作用域链是 JavaScript 用来确定变量访问权限的机制。它是在函数定义时就确定的,基于词法作用域(静态作用域),而不是函数调用时。
当一个函数被定义,它会"记住"自己所在的作用域环境,形成一个作用域链。在查找变量时,从当前作用域开始,逐层向外查找,直到全局作用域。
js
const x = 1;
function outer() {
const y = 2;
function inner() {
const z = 3;
console.log(x, y, z); // 1, 2, 3
}
inner();
}
outer();
作用域链变量查找
图解:
- A → B :
inner
执行时,创建执行上下文,其作用域链包含:inner
自身 →outer
→ 全局。 - C → D → E → F → G → H :
x
不在inner
或outer
中定义,最终在全局找到。 - I → J :
y
在outer
中定义,沿链找到。 - K → L :
z
在inner
中定义,直接使用。
关键点:
- 作用域链在函数定义时确定,与调用位置无关。
- 闭包的本质就是函数保留了对外部作用域的引用,即使外部函数已执行完毕。
3. call、apply、bind 的区别
问题: call
、apply
、bind
有什么区别?什么时候用哪个?
回答:
三者都用于改变函数执行时的 this
指向,但调用方式和返回值不同。
call(thisArg, arg1, arg2, ...)
: 立即执行函数,参数逐个传入。apply(thisArg, [argsArray])
: 立即执行函数,参数以数组形式传入。bind(thisArg, arg1, arg2, ...)
: 返回一个新函数,不会立即执行,可后续调用。
js
function greet(greeting, punctuation) {
console.log(`${greeting}, I'm ${this.name}${punctuation}`);
}
const person = { name: 'Bob' };
greet.call(person, 'Hi', '!'); // "Hi, I'm Bob!"
greet.apply(person, ['Hey', '?']); // "Hey, I'm Bob?"
const boundGreet = greet.bind(person, 'Hello');
boundGreet('.'); // "Hello, I'm Bob."
三者调用对比
图解:
call
和apply
都是立即调用,区别仅在参数形式。bind
是延迟绑定 ,返回一个this
已固定的新函数,适合事件回调、setTimeout 等场景。
实战建议:
- 数组参数用
apply
(如Math.max.apply(null, arr)
)。 - 需要预设部分参数时用
bind
(柯里化)。 call
更通用,参数明确时优先。
4. 防抖和节流的区别
问题: 防抖和节流有什么区别?分别适用什么场景?
回答:
两者都是控制函数执行频率的手段,用于优化高频触发事件(如 resize、scroll、input)。
- 防抖(Debounce):事件频繁触发时,只执行最后一次。如果持续触发,执行会被不断推迟。
- 节流(Throttle):事件触发后,在一定时间窗口内只执行一次,之后可再次执行。
js
// 防抖
function debounce(fn, delay) {
let timer = null;
return function (...args) {
clearTimeout(timer); // 🔍 清除上一次未执行的定时器
timer = setTimeout(() => {
fn.apply(this, args); // 🔍 保证 this 和参数正确传递
}, delay);
};
}
// 节流(定时器版)
function throttle(fn, delay) {
let timer = null;
return function (...args) {
if (timer) return; // 🔍 如果已有定时器,跳过本次
timer = setTimeout(() => {
fn.apply(this, args);
timer = null; // 🔍 执行后释放锁
}, delay);
};
}
Mermaid 状态图:防抖 vs 节流
图解:
- 防抖:每次触发都重置计时,只有"静默期"结束后才执行。适合搜索框输入、窗口停止调整后执行。
- 节流 :保证在
delay
时间内最多执行一次。适合滚动加载、按钮防重复点击。
踩坑提醒:
- 防抖在持续触发时可能永远不执行,需结合业务判断。
- 节流还有"时间戳版",性能更好但首次执行时机不同。
5. 介绍各种异步方案
问题: JS 异步发展经历了哪些阶段?Promise、async/await 解决了什么问题?
回答:
JS 异步经历了回调 → Promise → async/await 的演进,核心是解决"回调地狱"和错误处理问题。
js
// 1. 回调(Callback Hell)
getData(function (a) {
getMoreData(a, function (b) {
getEvenMoreData(b, function (c) {
console.log(c);
});
});
});
// 2. Promise(链式调用)
getData()
.then(a => getMoreData(a))
.then(b => getEvenMoreData(b))
.then(c => console.log(c))
.catch(err => console.error(err)); // 🔍 统一错误处理
// 3. async/await(同步写法)
async function fetchData() {
try {
const a = await getData();
const b = await getMoreData(a);
const c = await getEvenMoreData(b);
console.log(c);
} catch (err) {
console.error(err); // 🔍 错误冒泡,像同步代码一样处理
}
}
异步演进对比
图解:
- 回调:嵌套深,错误处理分散。
- Promise :扁平化,支持链式调用和
.catch()
。 - async/await :用同步语法写异步,可配合
try/catch
,可读性最强。
关键点:
Promise
是微任务,比setTimeout
(宏任务)优先执行。await
只能在async
函数内使用。- 错误必须用
try/catch
捕获,否则会变成未处理的Promise rejection
。
6. XSS 与 CSRF
问题: 说说 XSS 和 CSRF 的区别?如何防范?
回答:
XSS(跨站脚本攻击):攻击者注入恶意脚本,当其他用户浏览页面时执行,窃取 cookie、session 等。
CSRF(跨站请求伪造):攻击者诱导用户在已登录状态下发起非本意的请求,如转账、发帖。
html
<!-- XSS 示例:注入脚本 -->
<script>
fetch('/api/steal-cookie', {
method: 'POST',
body: document.cookie // 🔍 窃取用户 cookie
});
</script>
<!-- CSRF 示例:诱导提交表单 -->
<img src="http://bank.com/transfer?to=attacker&amount=1000" width="0">
XSS 与 CSRF 攻击路径
图解:
- XSS :攻击目标是用户浏览器,利用信任执行脚本。
- CSRF :攻击目标是服务器接口,利用用户身份伪造请求。
防范措施:
- XSS :
- 输入转义(HTML 实体编码)
- 使用 CSP(Content Security Policy)
- 设置
HttpOnly
cookie(防止 JS 读取)
- CSRF :
- 使用
SameSite
cookie 属性(推荐Strict
或Lax
) - 验证
Referer
/Origin
头 - 关键操作使用 Token(如 CSRF Token)
- 使用
7. HTTP 缓存控制
问题: HTTP 缓存有哪些机制?强缓存和协商缓存有什么区别?
回答:
HTTP 缓存分为强缓存 和协商缓存,浏览器优先使用强缓存,失效后再走协商缓存。
- 强缓存 :不发请求,直接使用本地缓存。由
Cache-Control
和Expires
控制。 - 协商缓存 :发请求,由服务器判断是否更新。由
ETag
/If-None-Match
或Last-Modified
/If-Modified-Since
实现。
http
# 响应头(服务器设置)
Cache-Control: max-age=3600
ETag: "abc123"
Last-Modified: Wed, 21 Oct 2023 07:28:00 GMT
图解:
- B → C :
max-age
未过期,直接读本地缓存,不发请求。 - D → E → F → H:强缓存过期,发请求,服务器发现资源未变,返回 304,浏览器继续用旧资源。
- D → E → G → I:资源已更新,返回新内容并更新缓存。
关键点:
Cache-Control
优先级高于Expires
。ETag
精度更高(内容哈希),Last-Modified
可能因秒级精度误判。- 静态资源建议设置长期强缓存 + 文件名哈希(如
app.a1b2c3.js
),避免更新问题。