首页
/ Front End Interview Handbook JavaScript 面试问题全解:this、闭包、继承与 ES6+ 核心考点精讲

Front End Interview Handbook JavaScript 面试问题全解:this、闭包、继承与 ES6+ 核心考点精讲

2026-09-08 20:04:15作者:农烁颖Land

本文基于 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 中最令人困惑的概念之一,没有单一简单解释——它的值取决于函数被调用的方式。以下是按优先级从高到低的六条规则:

  1. 若使用 new 关键字调用函数,函数内的 this 是一个全新创建的对象
  2. 若使用 applycallbind 调用/创建函数,函数内的 this作为参数传入的那个对象
  3. 若函数作为方法调用(如 obj.method()),this函数作为其属性的那个对象
  4. 若函数作为"自由函数"(free function)调用,即不满足上述任何条件,this 是全局对象;浏览器中为 window。若处于严格模式('use strict'),thisundefined 而非全局对象。
  5. 若上述多条规则同时适用,排位更高的规则胜出并决定 this 的值。
  6. 若函数是 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 接收参数数组。助记:callC = Comma(逗号),applyA = 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/.bindthis 始终指向构造出的那个对象。

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. letvarconst 创建的变量有何区别

  • 作用域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 语言本身的对象,如 StringMathRegExpObjectFunction
  • 主机对象由运行环境(浏览器或 Node)提供,如浏览器中的 windowXMLHTTPRequest

三、类型、相等性与缺失值判断

1. nullundefinedundeclared 的区别与检测

  • 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 同时判断 anullundefined——之外,一律使用 ===

var a = null;
console.log(a == null); // true
console.log(a == undefined); // true

3. 什么是 "use strict",其利与弊

'use strict' 是让整个脚本或单个函数进入严格模式的指令,用来约束 JavaScript 的种种宽松行为。

优点:

  • 防止意外创建全局变量;
  • 对本来"静默失败"的赋值(如只读属性赋值)改为抛异常;
  • 删除不可删除的属性会抛错(不再无声无息);
  • 要求函数参数名唯一;
  • 全局上下文中 thisundefined
  • 拦截若干常见易错写法;
  • 禁用容易混淆或不成熟的特性。

缺点:

  • 部分开发者不熟悉其规则;
  • 无法再访问 function.callerfunction.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 文档完全加载并解析完毕后触发,不等待样式表、图片与子框架;windowload 则在 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-infor (var property in obj) —— 会连继承属性一起遍历,使用前须用 obj.hasOwnProperty(property) 过滤;
  • Object.keys(obj):只列出对象自身的可枚举属性;
  • Object.getOwnPropertyNames(obj):列出对象自身**所有(含不可枚举)**属性。

数组

  • 传统 forfor (let i = 0; i < arr.length; i++)。注意 var 为函数作用域,通常需要块级作用域的迭代变量,因此用 let
  • forEacharr.forEach((el, index) => ...),无需索引时更简洁;需要提前终止可用 every/some
  • for-of:ES6 引入,可遍历实现可迭代协议的对象(StringArrayMapSet 等),兼具 forbreak 退出与 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 出某个值或拒绝(如网络错误)"的对象,处于 pendingfulfilledrejected 三态之一;使用者通过挂接回调处理成功值或失败原因。常见的 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/异步之外:XMLHttpRequestfetch

现代前端对上述异步能力的最佳实践是把 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)?

高阶函数是接收函数作为参数去处理数据、或返回函数作为结果的函数,用于抽象那些反复出现的操作。典型例子是接收数组与处理函数并返回新数组的 mapforEachfilterreduce 同样常见。返回函数的高阶函数则不必操作数组,如 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.createslice 或库函数;在 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.mdjavascript-utility-function.mdintroduction.md 等页面,从通用问答到手写工具函数延续同一条备考主线。)

九、如何在仓库中系统备考这些考点

以上问答并非孤立记忆,建议结合仓库内互为印证的资源分层复习:

  1. 对照英文原版与多语言版本查漏补缺:本问答的英文最新版是 website/contents/javascript-questions.md,其问题覆盖更广、若干回答还附带追加追问;韩文版即本文所依据的 website/i18n/kr/docusaurus-plugin-content-docs/current/javascript-questions.md。仓库在 docusaurus.config.js 中注册了 enzh-CNja-JPkrpt-BRrutlplesbn 等 locale,可在 website/i18n/ 下找到对应翻译互相参照。
  2. 配合 quiz 与 package 深化训练:顶层 questions/javascript-questions.md 是原始出题清单(本题集即其逐题解答);packages/front-end-interview-guidebook/contents/javascript/ 提供系统化的主题内容;packages/quiz/questions/ 收录大量带标准答案的判断题/选择题,适合自测。
  3. 结合博客理解真实面试语境:如 website/blog/2021-08-30-a-glimpse-into-front-end-interviews.mdwebsite/blog/2021-12-23-front-end-vs-back-end-system-design-interviews.md 描述了面试的真实形态与公司差异,帮助判断哪些考点值得重点投入。

结语:从"背答案"到"讲原理"

回看本文覆盖的全部问题,可以提炼出 Front End Interview Handbook 反复强调的应答策略:先给结论,再给依据,最后给代码与适用边界this 六条规则、==/===var/let/const、函数声明与表达式……这些看似分散的考点最终都汇聚到同一个能力上——准确描述 JavaScript 的运行时语义。把本文的代码逐一在控制台跑一遍、再对照英文版与 quiz 自测,你就能把这些高频面试题内化成可以现场推演的知识,而不是临时背诵的台词。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391