☰
FAQGURU JavaScript 面试题精讲:类型系统、作用域、严格模式与算法实战
2026/10/14 18:03:44 网站建设 项目流程
  • 教程

【免费下载链接】FAQGURU

:school_satchel: :rocket: :tada: A list of interview questions. This repository is everything you need to prepare for your technical interview.

项目地址:https://gitcode.com/gh_mirrors/fa/FAQGURU
点击查看免费下载

本文以 FAQGURU 开源仓库中的俄语版 JavaScript 面试题库(topics/ru/javascript.md)为主体,系统梳理 JavaScript 面试中最高频的考点:类型系统与typeof、显式/隐式类型转换、==与===的相等性规则、函数作用域与 ES6 的let、回调与闭包、严格模式、Polyfill,以及 5 道高频编码题(缺失数字、数组去重、字符串反转、闭包求和、双栈实现队列)。读完本文,你将掌握这些概念的可复现代码示例、复杂度分析,以及回答面试官追问时所需的底层原理。

文档背景与使用方式

FAQGURU 是一个面向技术面试准备的问答集合仓库,根目录 readme.md 对其定位有明确说明:收录了大量广受欢迎的面试题、答案与代码片段,其中许多题目来自真实面试现场;答案相对简短,本质上是"复习参考工具",适合在面试前快速过一遍核心考点。

本文所依据的 topics/ru/javascript.md 是仓库的俄语翻译版本,精选了 18 道 JavaScript 核心问题,涵盖语言基础与编码实战两大板块;其对应的英文原版 topics/en/javascript.md 内容更全(还包含事件冒泡、原型继承、模块模式、二分搜索等话题),可作为继续深挖同一主题的补充材料。仓库结构上,topics/ru/readme.md 汇总了俄语版全部主题目录,根目录 readme.md 则给出了完整的多语言主题索引。

需要提醒的是,如 readme.md 所强调:这些面试题大多是开放式的,直接背诵答案不如理解背后的原理——与面试官的深入讨论往往比"标准答案"更能体现能力。

一、JavaScript 类型系统

1.1 值与类型的七个内置类型

原文档明确指出一个关键结论:JavaScript 拥有"有类型的值",而不是"有类型的变量"——变量本身不绑定类型,可以随时被赋值为任意类型的值,真正决定类型的是当前持有的值。JavaScript 提供以下内置类型:

  • string(字符串)
  • number(数值)
  • boolean(布尔)
  • null与undefined
  • object(对象)
  • symbol(ES6 新增)

其中null、undefined常被合称为"底层类型"(bottom types),但在语义上两者不同:undefined表示"尚未初始化",null表示"当前不可用"。symbol是 ES6 引入的新原始类型,常用于生成独一无二的键。

1.2 typeof 运算符

typeof是一个一元运算符,用于检查一个值并返回其类型名称的字符串。原文档给出了完整的运行示例:

var a; typeof a; // "undefined" a = "hello world"; typeof a; // "string" a = 42; typeof a; // "number" a = true; typeof a; // "boolean" a = null; typeof a; // "object" -- странно, ошибка(奇怪,这是个 bug) a = undefined; typeof a; // "undefined" a = { b: "c" }; typeof a; // "object"

需要特别注意的是typeof null返回"object":null本身是独立的原始类型,但由于早期 ECMAScript 规范的遗留 bug,typeof对它返回"object"。原文档直接将其标注为"错误/奇怪的行为",这是面试官非常喜欢追问的细节——正确区分null与undefined(见 1.1),以及知道这个历史遗留问题,是回答本题的关键。

1.3 对象类型与两种属性访问方式

对象类型(object type)指的是复合值:可以在对象上设置属性(具名的存储位置),每个属性可以保存任意类型的值。原文档示例:

var obj = { a: "hello world", // 属性 b: 42, c: true }; obj.a; // "hello world",点号记法访问 obj.b; // 42 obj.c; // true obj["a"]; // "hello world",方括号记法访问 obj["b"]; // 42 obj["c"]; // true

点号记法(.a)更简洁,而方括号记法(["a"])的独特价值在于:当属性名保存在另一个变量中时,只能使用方括号。原文档给出了典型场景:

var obj = { a: "hello world", b: 42 }; var b = "a"; obj[b]; // "hello world"——b 的值是 "a",所以访问的是属性 a obj["b"]; // 42——直接访问属性 b

1.4 数组:特殊的对象

数组(array)本质上也是一种对象,区别在于它的值不是存放在具名的属性/键中,而是存放在数值索引的位置上:

var arr = [ "hello world", 42, true ]; arr[0]; // "hello world" arr[1]; // 42 arr[2]; // true arr.length; // 3 typeof arr; // "object"

从typeof arr返回"object"可以看出:数组只是对象的一种特殊形态。数组元素可以是任意类型(混合类型也允许),length属性由引擎自动维护。这个知识点常与 1.2 联动考察——"如何判断一个对象是不是数组",可以从Object.prototype.toString、Array.isArray等角度展开(英文原版 topics/en/javascript.md 对该题有更完整的解答)。

二、类型转换(Coercion)与相等性

2.1 显式转换与隐式转换

在 JavaScript 中,两个不同内置类型之间的转换被称为coercion(强制转换),分为两种形式:显式(explicit)与隐式(implicit)。

显式转换的典型例子——主动调用Number():

var a = "42"; var b = Number( a ); a; // "42" b; // 42 -- 数值!

隐式转换的典型例子——通过运算符触发自动转换:

var a = "42"; var b = a * 1; // "42" 在这里被隐式转换为 42 a; // "42" b; // 42 -- 数值!

a * 1中,乘法运算符要求两侧都是数值,因此引擎将字符串"42"隐式转换为数字42再参与运算。理解这两种转换的区别,是理解 2.2 中==与===差异的前提。

2.2 == 与 === 的差异

JavaScript 同时提供两种相等比较:

  • 严格比较(===):比较值是否相等,不允许发生coercion(强制转换);
  • 抽象比较(==):比较值是否相等,允许发生coercion。
var a = "42"; var b = 42; a == b; // true("42" 被转换为 42 后相等) a === b; // false(类型不同,直接不等)

上面的例子是面试中几乎必考的一对:"42" == 42为true,而"42" === 42为false。==在比较前会尝试把两侧值转换成相同类型;===则要求类型与值都相等。

2.3 三条实用的相等性经验法则

原文档给出了三条被广泛引用的实用规则,用于决定何时安全使用==:

  1. 如果比较的任何一侧可能为true或false,避免使用==,改用===——布尔值参与==时会先被转换为数字,极易产生出乎意料的真值判断;
  2. 如果比较的任何一侧可能是0、""(空字符串)或[](空数组)这些特定值,避免使用==,改用===——这三个值在强制转换中都属于"敏感值",[] == false为true等反直觉结果正是此类陷阱;
  3. 其余情况下可以放心使用==——不仅安全,而且在很多场景下能让代码更简洁、可读性更好。

换句话说:===是保守稳妥的默认选择,而==在排除掉真值、0/空串/空数组这类边界后,是一个被官方资料认可的安全且可读的选项。

三、作用域与变量声明

3.1 函数作用域

原文档对作用域(scope)的定义很清晰:每个函数都有自己的作用域。作用域本质上是"变量的集合 + 按名称访问这些变量的规则"。只有该函数内部的代码才能访问函数作用域内的变量。

两个关键推论:

  • 同一作用域内变量名必须唯一;
  • 作用域可以嵌套。如果某个作用域嵌套在另一个作用域中,最内层作用域的代码可以访问任何外层作用域的变量(逐层向外查找)。

这正是后面 4.2 闭包示例能够工作的基石:内层函数之所以能访问外层函数的baseNumber,就是因为嵌套作用域允许由内向外查找变量。

3.2 块级作用域:let 关键字

除了在函数级别创建变量声明之外,ES6 允许用let关键字声明归属于单个代码块(一对{ .. })的变量:

{ let blockScoped = 1; var functionScoped = 2; } // blockScoped 在这里不可访问(已超出块作用域) // functionScoped 在这里仍然可以访问(var 的作用域是函数级)

let与var的核心区别在于:var声明的变量属于整个函数作用域(并伴随提升,hoisting),而let声明的变量只在最近的{ .. }块内有效,并且存在"暂时性死区"(TDZ)——在声明语句之前访问会抛错而非得到undefined。对于面试回答,可以概括为:let让变量声明的作用域更精确,从"函数级"细化为"块级"。原文档在该节还注明了理论出处为 You-Dont-Know-JS 系列(getify 的开源书籍)的 Up & Going 第 2 章,面试前值得延伸阅读。

四、函数与回调

4.1 回调函数的概念与示例

callback(回调)是作为参数传递给另一个函数、并在某个操作完成后被执行的函数。这是 JavaScript 异步与高阶函数编程的核心概念。原文档给出了一个完整示例:

function modifyArray(arr, callback) { // 对数组做一些操作 arr.push(100); // 然后执行传入的回调函数 callback(); } var arr = [1, 2, 3, 4, 5]; modifyArray(arr, function() { console.log("数组已被修改", arr); });

执行后控制台输出数组已被修改 [1, 2, 3, 4, 5, 100]。注意回调在arr.push(100)之后才被调用,这正是"回调在操作完成后执行"语义的体现。面试中常见追问包括:回调的常见使用场景(事件监听、setTimeout、数组遍历方法、网络请求完成处理)、回调地狱(callback hell)及其解决方案(Promise / async-await)。

4.2 闭包实战:createBase

原文档用一道著名面试题演示闭包:编写一个函数,使得调用方式为addSix(10)、addSix(21)时分别返回 16、27。

可以创建一个闭包,让createBase收到的值在内层函数返回后依然被保留。返回的内层函数是在外层函数内部创建的,因此它构成了闭包,可以访问外层函数内的变量——这里的baseNumber:

function createBase(baseNumber) { return function(N) { // 这里引用了 baseNumber,尽管它是在本函数之外声明的。 // 闭包让我们可以在 JavaScript 中这样做 return baseNumber + N; } } var addSix = createBase(6); addSix(10); // 16 addSix(21); // 27

addSix持有的是createBase(6)返回的那个内层函数,同时它"记住"了外层作用域中的baseNumber = 6,因此每次调用都能完成6 + N的运算。闭包的"记住外部变量"特性,与 3.1 的嵌套作用域规则直接呼应——内层函数可以访问外层函数作用域的变量,且该变量不会被垃圾回收,因为内层函数仍然引用着它。

五、严格模式

5.1 什么是严格模式

严格模式(Strict Mode)是 ECMAScript 5 引入的特性,允许把整个程序或某个函数放入"严格"的工作上下文中。这个严格上下文会阻止某些动作的执行,并抛出更多异常,从而帮助开发者尽早发现代码中的问题。

严格模式可以作用于整个脚本或单个函数。典型的做法是用 IIFE(立即执行函数表达式)把第三方库包裹起来,使库代码运行在严格模式下、同时不影响库外的非严格代码:

// 非严格代码... (function(){ "use strict"; // 在这里严格地定义你的库... })(); // 非严格代码...

这种"函数级启用、局部生效"的方式,保证了严格模式不会污染全局环境——这是面试中关于严格模式的高频追问点。

5.2 "use strict" 的作用

"use strict"指令写在 JavaScript 程序顶部或函数顶部,通过在误创建全局变量时抛出错误来帮助你写出更安全的代码。原文档给出了经典示例:下面的程序会抛出错误——

function doSomething(val) { "use strict"; x = val + 10; }

它会报错,是因为x没有被声明,却试图在全局作用域中被赋值为某个值,这在严格模式下是不被允许的(非严格模式下这会静默创建一个隐式全局变量)。下面这个微小的修改即可修复错误:

function doSomething(val) { "use strict"; var x = val + 10; }

加上var显式声明后,x成为函数局部变量,不再触碰全局作用域,错误消失。面试中可以进一步补充严格模式的其他行为差异:禁止为未声明变量赋值、this在普通函数调用中不再默认指向全局对象(而是undefined)、禁止删除不可删除属性、禁止重复参数名等。注意原文档该节的示例代码中有一个笔误(多余的结尾反引号),上文已给出修正后的可运行版本。

六、Polyfill:兼容性工程

Polyfill(垫片)本质上是这样一段特定代码(或插件):当你期望某个功能在现代浏览器中可用,而另一些浏览器没有对该功能的内置支持时,Polyfill 让这个功能在这些老旧浏览器中同样可以工作。原文档还给出了两条常被追问的要点:

  • Polyfill 并不是 HTML5 标准的一部分——它只是用已有语言特性"模拟"标准行为的实现手段;
  • Polyfilling 并不局限于 JavaScript——任何需要填补兼容性缺口的技术领域都可以使用类似思路。

补充理解:Polyfill 是 shim(拦截 API 调用并提供抽象层的代码)的一种特殊形式,专门用于"用旧环境已有的手段,把新 API 带回旧环境"。面试中的典型例子是:在旧浏览器中为Array.prototype.map、String.prototype.includes、Object.assign等 ES5/ES6 方法编写降级实现,或者用Object.defineProperty模拟Array.isArray。相关延伸(shim 与 polyfill 的区别)在英文原版 topics/en/javascript.md 中有专节讨论。

七、数值判断:如何检查一个数是否为整数

一个非常简单的判断"小数或整数"的方法是:看它除以 1 之后是否还有余数。原文档给出的实现:

function isInt(num) { return num % 1 === 0; } console.log(isInt(4)); // true console.log(isInt(12.2)); // false console.log(isInt(0.3)); // false

原理:任何整数n % 1都等于0,而带小数部分的值对 1 取模会得到非零余数。这道题的考察点在于%(取余)运算符的理解,以及"整除"概念的灵活运用。

面试延伸:现代环境推荐使用更严谨的Number.isInteger()(它不会先把参数强制转换为数字,对NaN、Infinity、字符串等一律返回false);而% 1方案在极大数值、浮点精度边界上可能出现误判,回答时可以主动提及这一局限性以展示深度。原文档该节还标注了出处为 coderbyte 的 10 个常见 JavaScript 面试题专栏。

八、经典算法与编码题

8.1 O(n) 时间找出缺失的数

题目:一个未排序数组中包含 n 个连续整数中的 (n − 1) 个(上下界已知),如何在 O(n) 时间内找出缺失的那个数?原文档给出的完整解法:

// 函数输出应为 8 var arrayOfIntegers = [2, 5, 1, 4, 9, 6, 3, 7]; var upperBound = 9; var lowerBound = 1; findMissingNumber(arrayOfIntegers, upperBound, lowerBound); // 8 function findMissingNumber(arrayOfIntegers, upperBound, lowerBound) { // 遍历数组求出数字之和 var sumOfIntegers = 0; for (var i = 0; i < arrayOfIntegers.length; i++) { sumOfIntegers += arrayOfIntegers[i]; } // 利用高斯求和公式的变体求理论总和 // 公式:[(N * (N + 1)) / 2] - [(M * (M - 1)) / 2] // N 是上界,M 是下界 upperLimitSum = (upperBound * (upperBound + 1)) / 2; lowerLimitSum = (lowerBound * (lowerBound - 1)) / 2; theoreticalSum = upperLimitSum - lowerLimitSum; return theoreticalSum - sumOfIntegers; }

思路拆解:理论总和 − 实际总和 = 缺失的数。理论总和通过高斯求和公式的变体在 O(1) 内算出:

  • [1..N]的总和是N * (N + 1) / 2;
  • 减去[1..M-1]的总和(M-1) * M / 2,就得到[M..N]的理论总和。

整体时间复杂度 O(n)(一次遍历求和),空间复杂度 O(1)。几个值得向面试官说明的细节:

  1. 调用findMissingNumber(...)时函数声明尚未出现——之所以能工作,是因为**函数声明会被提升(hoisting)**到作用域顶部,这与 3.1 的作用域机制同源;
  2. 示例中upperLimitSum、lowerLimitSum、theoreticalSum都没有用var声明,在非严格模式下会变成隐式全局变量(回到 5.2 的话题:严格模式正是为了禁止这种行为);写出var声明是更严谨的写法;
  3. 该解法依赖于"连续整数、上下界已知"这一前提,面试中常见的变体是"n 个元素来自 [0, n] 的整数",同样可以用求和差或异或(XOR)求解。

8.2 数组去重

题目:删除数组中的重复元素,返回只包含唯一元素的数组。原文档同时给出了 ES6 与 ES5 两种实现:

// ES6 实现 var array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8]; Array.from(new Set(array)); // [1, 2, 3, 5, 9, 8] // ES5 实现 var array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8]; uniqueArray(array); // [1, 2, 3, 5, 9, 8] function uniqueArray(array) { var hashmap = {}; var unique = []; for(var i = 0; i < array.length; i++) { // 如果键返回 undefined(表示唯一),则被求值为假 if(!hashmap.hasOwnProperty(array[i])) { hashmap[array[i]] = 1; unique.push(array[i]); } } return unique; }

思路拆解:ES6 方案利用Set的"元素唯一"语义,再通过Array.from()转回数组,一行完成;ES5 方案用普通对象充当哈希表,hasOwnProperty检查当前元素是否第一次出现,是则记录并加入结果数组。两种方案的时间复杂度均为 O(n)。

可以补充的扩展点:ES6 还可以用展开语法[...new Set(array)]达到同样效果;哈希表方案存在"元素被对象键化(转为字符串)"的精度隐患(如数字 1 与字符串 "1" 会冲突),面试中可主动提出Map是更严格的替代方案。

8.3 反转句子中的每个单词

题目:给定一个字符串,反转其中的每个单词(单词内部字符顺序反转,单词间的相对顺序不变)。原文档的实现巧妙之处在于:把"反转"抽象为按分隔符拆分→反转→拼接:

var string = "Welcome to this Javascript Guide!"; // 输出变为 !ediuG tpircsavaJ siht ot emocleW var reverseEntireSentence = reverseBySeparator(string, ""); // 输出变为 emocleW ot siht tpircsavaJ !ediuG var reverseEachWord = reverseBySeparator(reverseEntireSentence, " "); function reverseBySeparator(string, separator) { return string.split(separator).reverse().join(separator); }

思路拆解:reverseBySeparator是一个通用函数——先按分隔符split成数组,再reverse反转顺序,最后用相同的分隔符join还原成字符串。

  • 第一次调用以空字符串""为分隔符:把整句拆成单个字符,反转后得到整句的完全倒序!ediuG tpircsavaJ siht ot emocleW;
  • 第二次调用以空格" "为分隔符:把倒序句按单词拆分(此时单词内的字母顺序已经颠倒),再整体反转单词顺序,得到emocleW ot siht tpircsavaJ !ediuG。

两步合起来的效果:单词间的顺序被还原,而每个单词内部的字母被翻转。这个"用同一函数、换分隔符"的技巧,体现了字符串处理中split/reverse/join三件套的灵活组合,是面试官考察代码复用意识的好题。

8.4 用两个栈实现队列(enqueue / dequeue)

题目:只使用两个栈实现队列的入队(enqueue)与出队(dequeue)操作。enqueue表示添加元素,dequeue表示移除元素。原文档给出的经典实现:

var inputStack = []; // 第一个栈 var outputStack = []; // 第二个栈 // 入队:直接把元素压入第一个栈 function enqueue(stackInput, item) { return stackInput.push(item); } function dequeue(stackInput, stackOutput) { // 反转栈:使输出栈的第一个元素成为输入栈的最后一个元素。 // 然后弹出输出栈的栈顶,得到最早被压入输入栈的那个元素 if (stackOutput.length <= 0) { while(stackInput.length > 0) { var elementToOutput = stackInput.pop(); stackOutput.push(elementToOutput); } } return stackOutput.pop(); }

思路拆解:队列是 FIFO(先进先出),栈是 LIFO(后进先出)。用两个栈模拟队列的经典策略是"双栈倒腾":

  • enqueue:直接push进inputStack,O(1);
  • dequeue:若outputStack为空,则把inputStack的元素逐个pop出来再push进outputStack——此时outputStack的栈顶正好是最早入队的元素,pop即可;若outputStack非空,直接pop。

每个元素最多被移动两次(从输入栈弹出、压入输出栈),因此 dequeue 的均摊时间复杂度为 O(1)。这也是数据结构面试中"用受限数据结构组合出新行为"的代表性题目,考察对栈、队列两种抽象语义的理解。

总结

围绕 topics/ru/javascript.md 的 18 个考点,可以提炼出一条清晰的复习主线:先掌握"类型系统与转换"(内置类型、typeof、coercion、==/===),再理解"作用域与函数"(函数作用域、let块级作用域、回调、闭包),随后是"安全与兼容"(严格模式、Polyfill),最后用编码题检验综合运用能力(缺失数字、去重、字符串反转、闭包求和、双栈队列)。

这些知识点环环相扣:闭包依赖嵌套作用域规则,let是对作用域模型的精细化,严格模式的报错行为又反过来让"隐式全局变量"这类问题无所遁形。面试准备时建议对照英文原版 topics/en/javascript.md 继续拓展(事件冒泡、原型继承、this绑定、模块模式、二分搜索等题目),并将仓库根目录 readme.md 中的项目定位("面试复习参考工具")铭记于心——答案只是起点,能围绕开放性问题展开有深度的讨论,才是面试中真正的加分项。

  • 教程

【免费下载链接】FAQGURU

:school_satchel: :rocket: :tada: A list of interview questions. This repository is everything you need to prepare for your technical interview.

项目地址:https://gitcode.com/gh_mirrors/fa/FAQGURU
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询