Front End Interview Handbook JavaScript 面试问题全解:this、闭包、继承与 ES6+ 核心考点精讲
本文基于 Front End Interview Handbook(韩文版 JS 问答)整理,系统覆盖前端面试中出现频率最高的 JavaScript 知识问答:从
this绑定规则、原型继承、事件委托,到模块系统、异步编程、ES6+ 语法糖,逐一给出结论、理由与可直接运行的示例代码。该文档最初是为 h5bp Front-end Job Interview Questions 中 JS 问题的逐题解答,仓库英文原版见 website/contents/javascript-questions.md,配套英文/中文等十余种翻译,并以 quiz 与 package 形式沉淀于 packages/quiz/questions/。读完本文,你既能回答"概念是什么",也能答出"为什么"与"何时该用哪个",把常见 JS 面试题变成可验证的硬知识。
一、函数、作用域与调用方式:吃透 this
1. this 在 JavaScript 中如何工作
this 是 JavaScript 中最令人困惑的概念之一,没有单一简单解释——它的值取决于函数被调用的方式。以下是按优先级从高到低的六条规则:
- 若使用
new关键字调用函数,函数内的this是一个全新创建的对象。 - 若使用
apply、call或bind调用/创建函数,函数内的this是作为参数传入的那个对象。 - 若函数作为方法调用(如
obj.method()),this是函数作为其属性的那个对象。 - 若函数作为"自由函数"(free function)调用,即不满足上述任何条件,
this是全局对象;浏览器中为window。若处于严格模式('use strict'),this为undefined而非全局对象。 - 若上述多条规则同时适用,排位更高的规则胜出并决定
this的值。 - 若函数是 ES2015 箭头函数,则忽略以上全部规则,在创建时继承其外围作用域的
this。
在 ES6 中 this 行为最显著的变化即箭头函数的"词法 this":箭头函数沿用闭合的词法作用域,不再由调用方通过 .call/.apply 改变上下文。这在便利的同时也带来陷阱——例如大型遗留应用中若把普通函数盲改为箭头函数,依赖调用方注入上下文的库(如 jQuery 的事件处理器)将无法正确绑定 this。
2. function Person(){}、var person = Person()、var person = new Person() 有何区别
function Person(){}只是一条普通函数声明;首字母大写(PascalCase)只是"当构造函数用"的命名约定,并非语言强制。var person = Person()是普通函数调用而非构造。构造函数通常不return值,因此以普通函数方式调用会返回undefined并赋给变量——这是初学者的常见失误。var person = new Person()使用new运算符创建实例,该实例继承Person.prototype;等价写法还有Object.create(Person.prototype)。
function Person(name) {
this.name = name;
}
var person = Person('John');
console.log(person); // undefined
console.log(person.name); // TypeError: Cannot read property 'name' of undefined
var person = new Person('John');
console.log(person); // Person { name: "John" }
console.log(person.name); // "John"
3. foo() 前的声明形态:函数声明与函数表达式
// 函数声明:整个函数体被提升
foo(); // 'FOOOOO'
function foo() {
console.log('FOOOOO');
}
// 函数表达式:仅变量声明被提升,函数体不提升
foo(); // TypeError: foo is not a function
var foo = function () {
console.log('FOOOOO');
};
前者为函数声明,后者为函数表达式。核心差异在于**提升(hoisting)**范围:函数声明连函数体一起提升,函数表达式只提升变量声明,定义之前调用会抛出 TypeError。
4. .call、.apply 与 .bind 的差异
.call 与 .apply 都用于调用函数,第一个参数同为函数内的 this;区别只在后续传参:.call 接收逗号分隔的逐个参数,.apply 接收参数数组。助记:call 的 C = Comma(逗号),apply 的 A = Arguments(参数数组)。
function add(a, b) {
return a + b;
}
console.log(add.call(null, 1, 2)); // 3
console.log(add.apply(null, [1, 2])); // 3
Function.prototype.bind 则返回一个新函数:当它被调用时,this 被固定为传入的值,并且可以预先填充若干前导参数(偏函数)。它在"把类的方法作为回调传给其他函数"时最有价值,例如 React 类组件中把事件处理器传给子组件前进行绑定。
5. 构造函数里用箭头方法有什么好处
箭头函数作为方法的主要优势是:this 在函数创建那一刻被固化,之后无法再改变。因此无论把这个方法传给谁、如何 .call/.apply/.bind,this 始终指向构造出的那个对象。
const Person = function (firstName) {
this.firstName = firstName;
this.sayName1 = function () {
console.log(this.firstName);
};
this.sayName2 = () => {
console.log(this.firstName);
};
};
const john = new Person('John');
const dave = new Person('Dave');
john.sayName1.call(dave); // Dave(普通函数 this 被改写)
john.sayName2.call(dave); // John(箭头函数 this 不可变)
var sayNameFromWindow1 = john.sayName1;
sayNameFromWindow1(); // undefined(this 变为 window)
var sayNameFromWindow2 = john.sayName2;
sayNameFromWindow2(); // John
在 React 类组件场景中这尤其好用:若用普通函数定义点击处理器并把它作为 prop 传给子组件,通常要在构造函数里手动 bind;改用箭头函数定义方法后,this 自动取自词法上下文,无需再绑定。
6. IIFE:为什么 function foo(){ }(); 不工作
IIFE 即"立即调用函数表达式"(Immediately Invoked Function Expression)。JS 解析器会把 function foo(){ }(); 读成两条语句:function foo(){ }(函数声明)与 ();(试图调用一个未命名的函数,于是抛出 SyntaxError: Unexpected token ))。
修法是把函数变成表达式再调用,两种括号位置均可:
(function foo() {})();
(function foo() {}());
凡以 function 开头的语句都被视为声明;用 () 包裹后成为函数表达式,后面的 () 即可立即执行。这类函数不会暴露到全局作用域,且当不需要在函数体内自引用时还可以省略名字。
也可借助 void 运算符:void function foo(){ }();。但需注意 void 对表达式的求值结果恒为 undefined,若 IIFE 有返回值将无法取用:
const foo = void (function bar() {
return 'foo';
})();
console.log(foo); // undefined
7. 为什么要避免污染全局作用域
所有脚本都能访问全局作用域,若人人都在全局命名空间定义变量必然发生命名冲突。应使用模块模式(IIFE 或模块系统)把变量封装在局部命名空间内。
8. let、var、const 创建的变量有何区别
- 作用域:
var是函数作用域(在函数外声明则挂到全局对象);let/const是块级作用域(仅在其最近的一对花括号内可见,如函数体、if-else块、for循环)。
function foo() {
var bar = 'bar';
let baz = 'baz';
const qux = 'qux';
// 三者在此函数内都可见
}
console.log(bar); // ReferenceError: bar is not defined
if (true) {
var bar = 'bar';
let baz = 'baz';
}
console.log(bar); // "bar"(var 逃逸出 if 块)
console.log(baz); // ReferenceError: baz is not defined
- 提升:
var声明会被提升,变量可在声明前被引用(值为undefined);let/const不允许,声明前访问抛出 ReferenceError("temporal dead zone")。
console.log(foo); // undefined
var foo = 'foo';
console.log(baz); // ReferenceError: can't access lexical declaration 'baz' before initialization
let baz = 'baz';
- 重复声明:
var可重复声明同一变量不报错;let/const重复声明抛SyntaxError。 - 重新赋值:
let允许重新赋值;const不允许——对const变量重新赋值抛异常(注意const只锁绑定,不深冻结对象内部)。
let foo = 'foo';
foo = 'bar'; // 合法
const baz = 'baz';
baz = 'qux'; // 抛异常
二、继承、闭包与数据封装
1. 原型继承如何工作
除 Object.create(null) 创建的对象外,所有 JS 对象都有指向另一个对象的 __proto__ 属性,即该对象的"原型"。当访问某对象属性而该对象上没有时,引擎会沿 __proto__、__proto__ 的 __proto__……一路查找,直到命中或抵达原型链末端。这模拟了经典继承,但本质更像是委托(delegation)而非继承。
可结合仓库 packages/front-end-interview-guidebook/contents/javascript/ 的构造范例理解链条搭建:父构造函数在 prototype 上添加方法,子构造函数先以 .call 调用父构造函数初始化自有字段,再通过 Object.setPrototypeOf(Dog.prototype, Animal.prototype) 让子原型继承父原型,最后在子原型上追加自己的方法。
2. 闭包是什么,如何用、为何用
闭包是"函数 + 该函数声明时所在的词法环境"的组合。"词法"(lexical)意味着作用域由源码中声明的位置决定,而非调用位置。闭包允许函数在外层函数已经返回之后,仍然访问外层函数的变量与作用域链。
典型用途:
- 用闭包模拟数据私有性与私有方法,这是模块模式(Module Pattern)的基础;
- 实现部分应用 / 柯里化(currying)。
3. 为什么不该扩展内建 JavaScript 对象
"扩展内建对象"指往原生对象的 prototype 上添加属性/方法。看似方便实则危险:假如多个库都向 Array.prototype 添加同名 contains 方法,实现会互相覆盖;只要两者行为不一致,你的代码就会出问题。唯一被认可的例外是编写 polyfill——当某方法属于规范但旧浏览器未实现时,你提供自己的实现补齐缺口。
4. 代码如何组织(模块模式 vs 经典继承)
早期常使用 Backbone 以 OOP 方式组织:创建 Backbone 模型并把方法挂在模型上。模块模式至今仍然优秀,但当下主流是 React/Redux 所倡导的 Flux 单向数据流架构:用普通对象表达应用模型,用操作这些对象的纯工具函数处理逻辑;状态像其他 Redux 应用一样通过 action 与 reducer 变更。除非必要,避免经典继承;如需使用类,遵循"以组合优先、少用继承"的工程约定。
5. 静态类成员(static class members)为什么存在
静态成员(属性/方法)不与某个具体实例绑定,无论哪个实例引用它取值都相同。静态属性通常是配置类常量;静态方法通常是不依赖实例状态的纯工具函数。
6. 主机对象与内建(原生)对象的区别
- 内建对象是 ECMAScript 规范定义、属于 JavaScript 语言本身的对象,如
String、Math、RegExp、Object、Function。 - 主机对象由运行环境(浏览器或 Node)提供,如浏览器中的
window、XMLHTTPRequest。
三、类型、相等性与缺失值判断
1. null、undefined、undeclared 的区别与检测
- undeclared:从未用
var/let/const声明的标识符被赋值时产生,会隐式地成为全局变量;严格模式下赋值直接抛ReferenceError。它和全局变量一样有害,应完全避免;若要检测只能将其包进try/catch。
function foo() {
x = 1; // 严格模式下抛出 ReferenceError
}
foo();
console.log(x); // 1
- undefined:已声明但尚未赋值的变量,类型为
undefined;函数没有显式返回时,其返回结果赋值给变量也会得到undefined。检测应使用严格相等===或typeof x === 'undefined',不要用==——它对null也返回true,会误判。
var foo;
console.log(foo); // undefined
console.log(foo === undefined); // true
console.log(typeof foo === 'undefined'); // true
console.log(foo == null); // true,但这是错误用法
function bar() {}
var baz = bar();
console.log(baz); // undefined
- null:显式赋予的"无值"表示,与
undefined的区别在于它是被显式赋值的。检测只需foo === null;同样不要用==(它对undefined也返回true)。
var foo = null;
console.log(foo === null); // true
console.log(foo == undefined); // true,错误用法
个人实践建议:变量要么不声明,要么一声明就显式赋初值;暂不需要时赋 null。配合 linter 通常能拦截对 undeclared 变量的引用。
2. == 与 === 的区别
== 是抽象相等运算符,比较前会做类型转换;=== 是严格相等运算符,不做类型转换,类型不同直接返回 false。== 会带来一系列反直觉结果:
1 == '1'; // true
1 == [1]; // true
1 == true; // true
0 == ''; // true
0 == '0'; // true
0 == false; // true
建议:除了一种便捷场景——用 a == null 同时判断 a 是 null 或 undefined——之外,一律使用 ===。
var a = null;
console.log(a == null); // true
console.log(a == undefined); // true
3. 什么是 "use strict",其利与弊
'use strict' 是让整个脚本或单个函数进入严格模式的指令,用来约束 JavaScript 的种种宽松行为。
优点:
- 防止意外创建全局变量;
- 对本来"静默失败"的赋值(如只读属性赋值)改为抛异常;
- 删除不可删除的属性会抛错(不再无声无息);
- 要求函数参数名唯一;
- 全局上下文中
this为undefined; - 拦截若干常见易错写法;
- 禁用容易混淆或不成熟的特性。
缺点:
- 部分开发者不熟悉其规则;
- 无法再访问
function.caller与function.arguments; - 用不同严格模式编写、再合并的脚本可能彼此冲突。
总体而言,严格模式利大于弊——你几乎不需要依赖它禁止的那些特性,推荐默认开启。
4. "attribute" 与 "property" 的区别
attribute 定义在 HTML 标记中,property 则属于 DOM 对象。假设 HTML 有 <input type="text" value="Hello">:
const input = document.querySelector('input');
console.log(input.getAttribute('value')); // Hello(初始标记值)
console.log(input.value); // Hello
用户在文本框追加 "World!" 后:
console.log(input.getAttribute('value')); // Hello(attribute 未变)
console.log(input.value); // Hello World!(property 随交互更新)
即 attribute 反映初始/声明值,property 反映 DOM 的当前状态,二者可能脱节。
四、DOM、事件与浏览器机制
1. 事件委托(Event delegation)
事件委托是把事件监听器加在父元素而非每个后代元素上。由于事件会沿 DOM 冒泡,后代上触发的事件同样会触发父级监听器。收益有两点:
- 只需在父元素挂一个处理器,内存占用显著下降;
- 元素被移除时无需逐个解绑、新增元素时无需重新绑定。
2. 事件冒泡(Event bubbling)
当某个 DOM 元素上的事件被触发,若该元素绑定了监听器则先处理,随后事件向父级"冒泡",父级上同名事件再次被触发,如此一直冒到最顶层的 document。事件冒泡正是事件委托能工作的底层机制。
3. load 事件与 DOMContentLoaded 事件的区别
DOMContentLoaded 在初始 HTML 文档完全加载并解析完毕后触发,不等待样式表、图片与子框架;window 的 load 则在 DOM 与所有依赖资源(样式、图片等)都加载完成之后才触发。若初始化逻辑无需等待整页资源,应优先使用 DOMContentLoaded。
4. 何时用 document.write()?
document.write() 向 document.open() 打开的文档流写入文本。一旦页面加载完成后才执行它,会先调用 document.open 清空整个文档(连 <head> 与 <body> 一起抹掉),再以传入字符串替换之——因此它通常危险且易被误用。历史上曾用于"仅在启用 JavaScript 时加载样式"或借助 <script> 标签并行加载脚本并保持执行顺序;但这些理由如今都已过时,现代实现不需要 document.write()。
5. Feature Detection、Feature Inference、UA String 的区别
- Feature Detection(特性检测):根据浏览器是否支持某能力分支执行不同代码,让浏览器"总能工作而非报错":
if ('geolocation' in navigator) {
// 使用 navigator.geolocation
} else {
// 处理能力缺失
}
- Feature Inference(特性推断):与特性检测一样检查某个特性,却臆断其他函数也存在,例如用
getElementsByTagName的存在推断getElementById也可用。不推荐——特性检测更稳妥。
if (document.getElementsByTagName) {
element = document.getElementById(id); // 错误的推断
}
- UA String(用户代理字符串):浏览器自报的字符串,用于让网络对端识别应用类型、操作系统与软件版本,通过
navigator.userAgent读取。它难以解析且可被伪造(例如 Chrome 同时上报为 Chrome 与 Safari;要识别 Safari 得同时检查"含 Safari、不含 Chrome")。不要用它做特性判断。
6. Same-origin policy(同源策略)
同源策略阻止 JavaScript 跨域发起请求。这里的"源"由 URI 协议 + 主机名 + 端口 组合定义。该策略防止某页面的恶意脚本经由该页面 DOM 窃取其他网页的敏感数据。
7. 对象/数组遍历用什么语言结构
对象:
for-in:for (var property in obj)—— 会连继承属性一起遍历,使用前须用obj.hasOwnProperty(property)过滤;Object.keys(obj):只列出对象自身的可枚举属性;Object.getOwnPropertyNames(obj):列出对象自身**所有(含不可枚举)**属性。
数组:
- 传统
for:for (let i = 0; i < arr.length; i++)。注意var为函数作用域,通常需要块级作用域的迭代变量,因此用let; forEach:arr.forEach((el, index) => ...),无需索引时更简洁;需要提前终止可用every/some;for-of:ES6 引入,可遍历实现可迭代协议的对象(String、Array、Map、Set等),兼具for可break退出与forEach简洁的优点。若同时要索引和值,配合entries()与解构:
const arr = ['a', 'b', 'c'];
for (let [index, elem] of arr.entries()) {
console.log(index, ': ', elem);
}
多数场景下 .forEach 是首选,但需提前退出时用 for-of,需要更细粒度控制(如每轮迭代步长大于 1)时退回 for。
8. .forEach 与 .map() 的主要区别
两者都遍历数组元素并对每个元素执行回调,本质区别是返回值:
const a = [1, 2, 3];
const doubled = a.forEach((num, index) => {
// 对 num 或 index 做点什么
});
// doubled = undefined
const doubled = a.map((num) => num * 2);
// doubled = [2, 4, 6]
.map() 会基于回调结果构造并返回新数组。若需要结果、又不愿改动原数组,.map() 是明确选择;若只是要遍历副作用,forEach 即可。
9. 给数组去重的实际编程题
让 duplicate([1, 2, 3, 4, 5]) 输出 [1,2,3,4,5,1,2,3,4,5]:
function duplicate(arr) {
return arr.concat(arr);
}
10. 为什么叫 "Ternary"(三元)表达式
"Ternary" 意为"三元的"。三元/条件表达式接收三个操作数:测试条件、"then"表达式与 "else"表达式。它并非 JavaScript 独有,而是通用的运算符概念。
五、网络、跨域与异步编程
1. 尽可能详细地解释 Ajax
Ajax(Asynchronous JavaScript and XML)是用于构建异步 Web 应用的一组客户端技术。它让应用能在后台向服务器收发数据,而不打断现有页面的呈现与交互——把数据交换层与表现层解耦,页面可动态更新内容而无需整页刷新。虽然名字含 XML,如今实践中多用 JSON。常用载体是 XMLHttpRequest API,现代代码则越来越多使用 fetch() API。
2. 使用 Ajax 的利与弊
优点:
- 交互性更好——无需整页重载即可动态替换服务器新内容;
- 脚本与样式表只需请求一次,减少对服务器的连接数;
- 状态由页面自行管理——主容器页不重载,JS 变量与 DOM 状态得以保留;
- 本质上继承了 SPA 的大部分优点。
缺点:
- 动态网页难以被书签收藏;
- 禁用 JavaScript 的浏览器无法工作;
- 部分爬虫不执行 JavaScript,看不到由 JS 加载的内容;
- 本质上继承了 SPA 的大部分缺点。
3. JSONP 如何工作(与 Ajax 有何不同)
JSONP(JSON with Padding)是绕过浏览器跨域限制的旧式方法。由于同源策略禁止向跨源域发起普通 Ajax 请求,JSONP 改为通过 <script> 标签请求跨源地址,并通常带上 callback 查询参数(如 https://example.com?callback=printData);服务器把数据包裹在名为 printData 的函数调用中返回,客户端收到后执行。
<!-- 位于 https://mydomain.com -->
<script>
function printData(data) {
console.log(`My name is ${data.name}!`);
}
</script>
<script src="https://example.com?callback=printData"></script>
// 从 https://example.com?callback=printData 加载的文件内容
printData({ name: 'Yang Shun' });
客户端必须在全局作用域预置 printData 函数,跨源响应到达时由它执行。JSONP 可能不安全:它返回的是可执行的 JavaScript,能做 JS 能做的一切,因此你必须信任 JSONP 数据提供方。如今推荐采用 CORS,JSONP 被视为 hack。
4. 使用过 JavaScript 模板吗?用了哪些库?
常见方案有 Handlebars、Underscore、Lodash、AngularJS 与 JSX。AngularJS 的模板大量依赖指令内字符串,拼写错误难以被静态发现;JSX 更接近 JavaScript、需要学习的新语法少,因此更受青睐。如今最简单快速的方式是直接用 ES2015 模板字符串做轻量模板。但要注意:与模板库不同,模板字符串不会自动做 HTML 转义,直接拼入用户内容存在 XSS 风险。
5. 对 Promise 及其 polyfill 的经验
Promise 是"未来会 resolve 出某个值或拒绝(如网络错误)"的对象,处于 pending、fulfilled、rejected 三态之一;使用者通过挂接回调处理成功值或失败原因。常见的 polyfill 有 $.deferred、Q、Bluebird——但并非全部严格遵循规范。ES2015 已原生支持 Promise,如今一般不再需要 polyfill。
6. 用 Promise 替代 Callback 的利与弊
优点:
- 规避可读性极差的"回调地狱";
- 用
.then()链写出清晰的连续异步代码; - 用
Promise.all()轻松编写并行异步逻辑; - 消除纯回调风格中难以防范的问题:回调被过早/过晚/不被调用、调用次数过少或过多、未能传入所需上下文/参数、异常被静默吞掉。
缺点:
- 源码略微更复杂(有争议);
- 不支持 ES2015 的老浏览器需加载 polyfill。
7. 同步函数与异步函数的区别
同步函数是阻塞的:前一条语句执行完,下一条才会执行,程序严格按语句顺序求值;若某条语句耗时极长,整个程序(浏览器中即 UI)都会被卡住。异步函数通常经参数接收回调,调用后立即继续执行后续代码;回调要等异步操作完成且调用栈清空后才会被推入执行。像 Web 服务器取数、数据库查询这类重活应异步执行,让主线程不被长时间占用。
8. 什么是事件循环?调用栈与任务队列的区别
事件循环是单线程的循环,不断监视调用栈(call stack)并检查任务队列(task queue)中是否有待执行的工作。当调用栈为空且任务队列中有回调时,就把该函数从队列取出、推入调用栈执行。
9. Ajax/异步之外:XMLHttpRequest 与 fetch
现代前端对上述异步能力的最佳实践是把 XMLHttpRequest 换成 fetch() 并结合 Promise。仓库内的 blog/2019-11-03-are-front-end-development-enough-for-a-career.md 等文章从职业与趋势角度印证了异步、状态管理在今日前端中的核心地位。
六、模块系统、语言演进与"编译到 JS"的语言
1. AMD 与 CommonJS 的取舍
两者都是 ES2015 之前、JS 语言层面没有模块系统时的模块方案。CommonJS 是同步的,面向服务端设计;AMD(Asynchronous Module Definition)显然是异步的,因支持模块异步加载而更多用于浏览器。AMD 语法冗长;CommonJS 的 import 风格更接近多数语言。多数场景其实不需要 AMD——把所有 JS 打包成一个 bundle 文件提供时,异步加载的优势无从发挥;而 CommonJS 写法贴近 Node 风格,在客户端与服务端 JS 之间切换时上下文开销更小。ES2015 模块同时支持同步与异步加载,终于可以只用一种方案;尽管浏览器与 Node 尚未完全原生支持,总可以用转译器处理。
2. 如何在文件之间共享代码
取决于运行环境:
- 客户端(浏览器):变量/函数声明在全局作用域(
window)即可被所有脚本引用;更模块化的做法是经 RequireJS 使用 AMD; - 服务端(Node.js):通行做法是 CommonJS——每个文件是一个模块,把变量与函数挂到
module.exports上导出; - ES2015:定义了旨在取代 AMD 与 CommonJS 的模块语法,浏览器与 Node 均可使用。
3. 用"编译到 JavaScript 的语言"写 JS 的利与弊
典型语言包括 CoffeeScript、Elm、ClojureScript、PureScript 与 TypeScript。
优点:
- 解决 JS 的长期痛点、规避反模式;
- 提供语法糖,源码更短(ES5 不够好,ES2015 已很棒);
- 静态类型利于大型长期维护项目(TypeScript 场景)。
缺点:
- 浏览器只执行 JS,必须引入构建/编译步骤;
- 若 source map 映射不佳,调试困难;
- 团队需学习新语言,引入培训成本;
- 社区较小的语言难找到资源、教程、库与工具;
- IDE/编辑器支持可能不足;
- 语言特性总是落后于最新 JS 标准;
- 开发者必须清楚自己代码编译成了什么——那才是真正运行的东西。
实际上 ES2015 已大幅改善 JS 的书写体验,如今很难再感到需要 CoffeeScript。
4. 如何调试 JavaScript 代码
常用工具与技巧:
- React/Redux:React DevTools、Redux DevTools;
- 通用 JS:Chrome DevTools、
debugger语句、最朴素的console.log调试。
5. ES6 类与 ES5 函数构造函数的区别
简单构造场景二者几乎一样:
// ES5 函数构造函数
function Person(name) {
this.name = name;
}
// ES6 类
class Person {
constructor(name) {
this.name = name;
}
}
差异在继承时放大。要为 Person 添加带 studentId 的子类 Student:
// ES5
function Student(name, studentId) {
Person.call(this, name); // 调用父构造函数初始化继承成员
this.studentId = studentId; // 初始化子类成员
}
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;
// ES6
class Student extends Person {
constructor(name, studentId) {
super(name);
this.studentId = studentId;
}
}
ES5 手写继承繁琐且易错,ES6 版本更易理解与记忆。
七、数组、函数式技巧与 ES6+ 语法糖
1. 什么是高阶函数(Higher-order function)?
高阶函数是接收函数作为参数去处理数据、或返回函数作为结果的函数,用于抽象那些反复出现的操作。典型例子是接收数组与处理函数并返回新数组的 map;forEach、filter、reduce 同样常见。返回函数的高阶函数则不必操作数组,如 Array.prototype.bind。
const names = ['irish', 'daisy', 'anna'];
// 命令式写法
const transformNamesToUppercase = function (names) {
const results = [];
for (let i = 0; i < names.length; i++) {
results.push(names[i].toUpperCase());
}
return results;
};
// 声明式写法
const transformNamesToUppercase = (names) => names.map((name) => name.toUpperCase());
transformNamesToUppercase(names); // ['IRISH', 'DAISY', 'ANNA']
2. 柯里化(currying)的函数示例及其语法收益
currying 把一个接收多个参数的函数拆成一串依次调用、逐个累计参数的函数,让函数式代码更易读、更易组合。把普通函数柯里化,就是从单函数拆出每次只收一个参数的一串函数:
function curry(fn) {
if (fn.length === 0) {
return fn;
}
function _curried(depth, args) {
return function (newArgument) {
if (depth - 1 === 0) {
return fn(...args, newArgument);
}
return _curried(depth - 1, [...args, newArgument]);
};
}
return _curried(fn.length, []);
}
function add(a, b) {
return a + b;
}
var curriedAdd = curry(add);
var addFive = curriedAdd(5);
[0, 1, 2, 3, 4, 5].map(addFive); // [5, 6, 7, 8, 9, 10]
3. 可变与不可变对象:immutability 讨论
面试中常拆成三个子问题:"JS 中不可变对象的例子有哪些?""不可变性的利与弊是什么?""在自己的代码里如何实现不可变性?"(此题为开放式讨论,重点考察:String/数字等原始值天然不可变,Object.freeze 提供浅层冻结;不可变数据便于追踪变更、利于 React/Redux 这类状态管理,代价是复制开销与学习成本;实践中常用展开运算符/Object.assign/不可变库维持不可变更新,可对照 blog/2022-06-18-javascript-object-vs-map.md 中对对象与 Map 数据结构的取舍分析。)
4. 对象与数组的解构示例
解构(destructuring)是 ES6 提供的、从对象/数组中取出值并赋给变量的简洁表达式。
数组解构:
const foo = ['one', 'two', 'three'];
const [one, two, three] = foo;
console.log(one); // "one"
console.log(two); // "two"
console.log(three); // "three"
// 变量交换
let a = 1;
let b = 3;
[a, b] = [b, a];
console.log(a); // 3
console.log(b); // 1
对象解构:
const o = { p: 42, q: true };
const { p, q } = o;
console.log(p); // 42
console.log(q); // true
5. ES6 模板字面量(Template literals)的灵活用法
模板字面量让字符串插值变得简单。ES2015 之前要拼接:
var person = { name: 'Tyler', age: 28 };
console.log('Hi, my name is ' + person.name + ' and I am ' + person.age + ' years old!');
用模板字面量(注意是反引号而非引号,${} 内可放任意表达式):
const person = { name: 'Tyler', age: 28 };
console.log(`Hi, my name is ${person.name} and I am ${person.age} years old!`);
第二个用途是多行字符串,模板字面量会原样保留书写时的空白:
console.log(`This is line one.
This is line two.`);
第三个用途是替代轻量模板库做变量插值:
const person = { name: 'Tyler', age: 28 };
document.body.innerHTML = `
<div>
<p>Name: ${person.name}</p>
<p>Age: ${person.age}</p>
</div>
`;
使用 innerHTML 会引入 XSS 风险——若内容是用户输入,展示前务必做安全处理(转义/消毒)。
6. spread 语法与 rest 语法的区别与收益
spread 语法便于以函数式风格复制数组/对象,无需 Object.create、slice 或库函数;在 Redux、RxJS 项目中大量使用:
function putDookieInAnyArray(arr) {
return [...arr, 'dookie'];
}
// ["I", "really", "don't", "like", "dookie"]
const person = { name: 'Todd', age: 29 };
const copyOfTodd = { ...person };
rest 语法则提供收集"传给函数的任意数量实参"的简写——它把数据收敛成数组(与 spread 的展开方向相反),可用于函数参数、数组解构与对象解构:
function addFiveToABunchOfNumbers(...numbers) {
return numbers.map((x) => x + 5);
}
const result = addFiveToABunchOfNumbers(4, 5, 6, 7, 8, 9, 10); // [9, 10, 11, 12, 13, 14, 15]
const [a, b, ...rest] = [1, 2, 3, 4]; // a: 1, b: 2, rest: [3, 4]
const { e, f, ...others } = { e: 1, f: 2, g: 3, h: 4 };
// e: 1, f: 2, others: { g: 3, h: 4 }
7. 箭头函数 => 的语法收益与行为差异
箭头函数最直接的收益是不用 function 关键字即可定义函数,语法更简洁;更重要的是箭头函数内的 this 绑定到外围作用域(词法 this),而不是由调用对象决定。词法 this 在 React 组件中传递回调时尤其受用。
8. 匿名函数有哪些常见用途
- 作为 IIFE 把一段代码封装进局部作用域,避免内部变量泄漏到全局:
(function () {
// code
})();
- 作为只用一次、无需他处复用的回调;把处理器直接内联在调用代码旁,代码更自洽易读:
setTimeout(function () {
console.log('Hello world!');
}, 1000);
- 作为函数式编程或 Lodash 式 API 的参数(本质同上):
const arr = [1, 2, 3];
const double = arr.map(function (el) {
return el * 2;
});
console.log(double); // [2, 4, 6]
9. 经典编码题:打印 1 到 100 的 FizzBuzz
对 1~100 的每个数:是 3 的倍数打印 fizz,是 5 的倍数打印 buzz,同时是 3 与 5 的倍数打印 fizzbuzz。有一种取巧的浓缩写法:
for (let i = 1; i <= 100; i++) {
let f = i % 3 == 0,
b = i % 5 == 0;
console.log(f ? (b ? 'FizzBuzz' : 'Fizz') : b ? 'Buzz' : i);
}
但不建议在面试中这样炫技——应选择长而清晰的实现,可读性优先。
10. 什么是提升(hoisting)?
hoisting 描述变量声明在代码中的表现:用 var 声明的变量会"上移"到当前作用域顶部——但只有声明被提升,赋值(若存在)留在原处。准确地说声明并未真的移动,而是引擎在编译期解析声明、提前认识它们;用"移到作用域顶部"的心智模型更易理解行为:
// var 声明被提升
console.log(foo); // undefined
var foo = 1;
console.log(foo); // 1
// let/const 存在"暂时性死区",不提升
console.log(bar); // ReferenceError: bar is not defined
let bar = 2;
函数声明连函数体一起提升,而写成变量赋值的函数表达式只提升变量声明(前文"函数声明 vs 函数表达式"已演示)。
八、SPA 与相关面试延伸
1. 什么是单页应用(SPA)?如何做到 SEO 友好
传统 Web 中浏览器从服务器取 HTML 渲染,用户跳转 URL 需要整页刷新、服务器为每个新页面返回新 HTML,即服务端渲染。现代 SPA 改用客户端渲染:浏览器首次加载整份应用脚本(框架、库、业务代码)与样式表;用户切换页面不再刷新,URL 通过 HTML5 History API 更新,新数据以 JSON 形式经 AJAX 请求获取,页面由已下载的 JS 动态更新——与原生移动应用的工作方式类似。
优点:
- 页面切换间不再闪现白屏,应用响应感更强;
- 同一批静态资源无需每次加载都重新下载,对服务器的 HTTP 请求更少;
- 客户端与服务端关注点分离清晰——只要 API 契约不变,可在不修改服务端的前提下为移动端、聊天机器人、智能手表等构建新客户端,两端技术栈也可独立演进。
缺点:
- 首次加载要下载框架、应用代码与资源,首屏偏重;
- 需要额外把服务端配置为"所有请求路由到单一入口",再交由客户端路由接管;
- SPA 依赖 JS 渲染内容,而并非所有搜索引擎都在爬取时执行 JS,页面可能被看到空内容,损害 SEO。不过多数 App 无需让搜索引擎索引全部内容;需要时可通过服务端渲染或 Prerender 类服务(在浏览器中渲染 JS、保存静态 HTML、再回给爬虫)解决。
(注:仓库内 website/contents/ 另设 trivia.md、javascript-utility-function.md 与 introduction.md 等页面,从通用问答到手写工具函数延续同一条备考主线。)
九、如何在仓库中系统备考这些考点
以上问答并非孤立记忆,建议结合仓库内互为印证的资源分层复习:
- 对照英文原版与多语言版本查漏补缺:本问答的英文最新版是 website/contents/javascript-questions.md,其问题覆盖更广、若干回答还附带追加追问;韩文版即本文所依据的 website/i18n/kr/docusaurus-plugin-content-docs/current/javascript-questions.md。仓库在 docusaurus.config.js 中注册了
en、zh-CN、ja-JP、kr、pt-BR、ru、tl、pl、es、bn等 locale,可在 website/i18n/ 下找到对应翻译互相参照。 - 配合 quiz 与 package 深化训练:顶层 questions/javascript-questions.md 是原始出题清单(本题集即其逐题解答);packages/front-end-interview-guidebook/contents/javascript/ 提供系统化的主题内容;packages/quiz/questions/ 收录大量带标准答案的判断题/选择题,适合自测。
- 结合博客理解真实面试语境:如 website/blog/2021-08-30-a-glimpse-into-front-end-interviews.md 与 website/blog/2021-12-23-front-end-vs-back-end-system-design-interviews.md 描述了面试的真实形态与公司差异,帮助判断哪些考点值得重点投入。
结语:从"背答案"到"讲原理"
回看本文覆盖的全部问题,可以提炼出 Front End Interview Handbook 反复强调的应答策略:先给结论,再给依据,最后给代码与适用边界。this 六条规则、==/===、var/let/const、函数声明与表达式……这些看似分散的考点最终都汇聚到同一个能力上——准确描述 JavaScript 的运行时语义。把本文的代码逐一在控制台跑一遍、再对照英文版与 quiz 自测,你就能把这些高频面试题内化成可以现场推演的知识,而不是临时背诵的台词。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00