人们认为 JavaScript 是最适合初学者的语言. 一部分原因在于 JavaScript 在互联网中运用广泛, 另一部分原因在于其自身特性使得即使编写的代码不那么完美依然可以运行: 无论是否少了一个分号或是内存管理问题, 它都不像许多其他语言那样严格, 但在开始学习之前, 要确保你已经知道 JavaScript 的来龙去脉, 包括可以自动完成的事情和 "幕后" 的操作.
本文将介绍一些面试时关于 JavaScript 的常见问题, 以及一些突发难题. 当然, 每次面试都是不同的, 你也可能不会遇见这类问题. 但是知道的越多, 准备的就越充分.
第一部分: 突发难题
如果在面试中突然问到下列问题, 似乎很难回答. 即便如此, 这些问题在准备中仍发挥作用: 它们揭示了 JavaScript 的一些有趣的功能, 并强调在提出编程语言时, 首先必须做出的一些决定.
了解有关 JavaScript 的更多功能, 建议访问 https://wtfjs.com.
1. 为什么 Math.max()小于 Math.min()?
Math.max()> Math.min()输出错误这一说法看上去有问题, 但其实相当合理.
如果没有给出参数, Math.min()返回 infinity(无穷大),Math.max()返回 - infinity(无穷小). 这只是 max()和 min()方法规范的一部分, 但选择背后的逻辑值得深议. 了解其中原因, 请看以下代码:
- Math.min(1) // 1
- Math.min(1, infinity)// 1
- Math.min(1, -infinity)// -infinity
如果 - infinity(无穷小)作为 Math.min()的默认参数, 那么每个结果都是 - infinity(无穷小), 这毫无用处! 然而, 如果默认参数是 infinity(无穷大), 则无论添加任何参数返回都会是该数字 - 这就是我们想要的运行方式.
2. 为什么 0.1+0.2 不等于 0.3
简而言之, 这与 JavaScript 在二进制中存储浮点数的准确程度有关. 在 Google Chrome 控制台中输入以下公式将得到:
- 0.1 + 0.2// 0.30000000000000004
- 0.1 + 0.2 - 0.2// 0.10000000000000003
- 0.1 + 0.7// 0.7999999999999999
如果是简单的等式, 对准确度没有要求, 这不太可能产生问题. 但是如果需要测试相等性, 即使是简单地应用也会导致令人头疼的问题. 解决这些问题, 有以下几种方案.
(1) Fixed Point 固定点
例如, 如果知道所需的最大精度 (例如, 如果正在处理货币), 则可以使用整数类型来存储该值. 因此, 可以存储 499 而非 4.99 美元, 并在此基础上执行任何等式, 然后可以使用类似 result =(value / 100).toFixed(2) 的表达式将结果显示给最终用户, 该表达式返回一个字符串.
(2) BCD 代码
如果精度非常重要, 另一种方法是使用二进制编码的十进制 (BCD) 格式, 可以使用 BCD 库 (https://formats.kaitai.io/bcd/javascript.html) 访问 JavaScript. 每个十进制值分别存储在一个字节 (8 位) 中. 鉴于一个字节可以存储 16 个单独值, 而该系统仅使用 0-9 位, 所以这种方法效率低下. 但是, 如果十分注重精确度, 采用何种方法都值得考量.
3. 为什么 018 减 017 等于 3?
018-017 返回 3 实际是静默类型转换的结果. 这种情况, 讨论的是八进制数.
(1) 八进制数简介
你或许知道计算中使用二进制 (base-2) 和十六进制 (base-16) 数字系统, 但是八进制 (base-8) 在计算机历史中的地位也举足亲重: 在 20 世纪 50 年代后期和 20 世纪 60 年代间, 八进制被用于简化二进制, 削减高昂的制造系统中的材料成本.
不久以后 Hexadecimal(十六进制)开始登上历史舞台:
1965 年发布的 IBM360 迈出了从八进制到十六进制的决定性一步。我们这些习惯八进制的人对这一举措感到震惊!
沃恩 · 普拉特 (Vaughan Pratt)
|
(2) 如今的八进制数
但在现代编程语言中, 八进制又有何作用呢? 针对某些案例, 八进制比十六进制更具优势, 因为它不需要任何非数字(使用 0-7 而不是 0-F).
一个常见用途是 Unix 系统的文件权限, 其中有八个权限变体:
- 4 2 1
- 0 - - - no permissions
- 1 - - x only execute
- 2 - x - only write
- 3 - x x write and execute
- 4 x - - only read
- 5 x - x read and execute
- 6 x x - read and write
- 7 x x x read, write and execute
出于相似的原由, 八进制也用于数字显示器.
(3) 回到问题本身
在 JavaScript 中, 前缀 0 将所有数字转换为八进制. 但是, 八进制中不使用数字 8, 任何包含 8 的数字都将自动转换为常规十进制数.
因此, 018-017 实际上等同于十进制表达式: 18-15, 因为 017 使用八进制而 018 使用十进制.
第二部分: 常见问题
本节中, 将介绍面试中一些更加常见的 JavaScript 问题. 第一次学习 JavaScript 时, 这些问题容易被忽略. 但在编写最佳代码时, 了解下述问题用处颇大.
4. 函数表达式与函数声明有哪些不同?
函数声明使用关键字 function, 后跟函数的名称. 相反, 函数表达式以 var,let 或 const 开头, 后跟函数名称和赋值运算符 =. 请看以下代码:
- // Function Declaration
- function sum(x, y) {
- return x + y;
- };
- // Function Expression: ES5
- var sum = function(x, y) {
- return x + y;
- };
- // Function Expression: ES6+
- const sum = (x, y) => { return x + y };
实际操作中, 关键的区别在于函数声明要被提升, 而函数表达式则没有. 这意味着 JavaScript 解释器将函数声明移动到其作用域的顶部, 因此可以定义函数声明并在代码中的任何位置调用它. 相比之下, 只能以线性顺序调用函数表达式: 必须在调用它之前解释.
如今, 许多开发人员偏爱函数表达式有如下几个原因:
首先, 函数表达式实施更加可预测的结构化代码库. 当然, 函数声明也可使用结构化代码库; 只是函数声明让你更容易摆脱凌乱的代码.
其次, 可以将 ES6 语法用于函数表达式: 这通常更为简洁, let 和 const 可以更好地控制是否重新赋值变量, 我们将在下一个问题中看到.
5. var,let 和 const 有什么区别?
自 ES6 发布以来, 现代语法已进入各行各业, 这已是一个极其常见的面试问题. Var 是第一版 JavaScript 中的变量声明关键字. 但它的缺点导致在 ES6 中采用了两个新关键字: let 和 const.
这三个关键字具有不同的分配, 提升和域 - 因此我们将单独讨论.
(1) 分配
最基本的区别是 let 和 var 可以重新分配, 而 const 则不能. 这使得 const 成为不变变量的最佳选择, 并且它将防止诸如意外重新分配之类的失误. 注意, 当变量表示数组或对象时, const 确实允许变量改变, 只是无法重新分配变量本身.
Let 和 var 都可重新分配, 但是正如以下几点应该明确的那样, 如果不是所有情况都要求更改变量, 多数选择中, let 具有优于 var 的显著优势.
(2) 提升
与函数声明和表达式 (如上所述) 之间的差异类似, 使用 var 声明的变量总是被提升到它们各自的顶部, 而使用 const 和 let 声明的变量被提升, 但是如果你试图在声明之前访问, 将会得到一个 TDZ(时间死区)错误. 由于 var 可能更容易出错, 例如意外重新分配, 因此运算是有用的. 请看以下代码:
- var x = "global scope";
- function foo() {
- var x = "functional scope";
- console.log(x);
- }
- foo(); // "functional scope"
- console.log(x); // "global scope"
这里, foo()和 console.log(x)的结果与预期一致. 但是, 如果去掉第二个变量又会发生什么呢?
- var x = "global scope";
- function foo() {
- x = "functional scope";
- console.log(x);
- }
- foo(); // "functional scope"
- console.log(x); // "functional scope"
尽管在函数内定义, 但 x ="functional scope" 已覆盖全局变量. 需要重复关键字 var 来指定第二个变量 x 仅限于 foo().
(3) 域
虽然 var 是 function-scoped(函数作用域), 但 let 和 const 是 block-scoped(块作用域的: 一般情况下, Block 是大括号 {} 内的任何代码, 包括函数, 条件语句和循环. 为了阐明差异, 请看以下代码:
- var a = 0;
- let b = 0;
- const c = 0;
- if (true) {
- var a = 1;
- let b = 1;
- const c = 1;
- }
- console.log(a); // 1
- console.log(b); // 0
- console.log(c); // 0
在条件块中, 全局范围的 var a 已重新定义, 但全局范围的 let b 和 const c 则没有. 一般而言, 确保本地任务保持在本地执行, 将使代码更加清晰, 减少出错.
6. 如果分配不带关键字的变量会发生什么?
如果不使用关键字定义变量, 又会如何? 从技术上讲, 如果 x 尚未定义, 则 x = 1 是 Windows.x = 1 的简写.
要想完全杜绝这种简写, 可以编写严格模式,-- 在 ES5 中介绍过 -- 在文档顶部或特定函数中写 use strict. 后, 当你尝试声明没有关键字的变量时, 你将收到一条报语法错误: Uncaught SyntaxError:Unexpected indentifier.
7. 面向对象编程 (OOP) 和函数式编程 (FP) 之间的区别是什么?
JavaScript 是一种多范式语言, 即它支持多种不同的编程风格, 包括事件驱动, 函数和面向对象.
编程范式各有不同, 但在当代计算中, 函数编程和面向对象编程最为流行 - 而 JavaScript 两种都可执行.
(1) 面向对象编程
OOP 以 "对象" 这一概念为基础的数据结构, 包含数据字段 (JavaScript 称为类) 和程序(JavaScript 中的方法).
一些 JavaScript 的内置对象包括 Math(用于 random,max 和 sin 等方法),JSON(用于解析 JSON 数据)和原始数据类型, 如 String,Array,Number 和 Boolean.
无论何时采用的内置方法, 原型或类, 本质上都在使用面向对象编程.
(2) 函数编程
FP(函数编程)以 "纯函数" 的概念为基础, 避免共享状态, 可变数据和副作用. 这可能看起来像很多术语, 但可能已经在代码中创建了许多纯函数.
输入相同数据, 纯函数总是返回相同的输出. 这种方式没有副作用: 除了返回结果之外, 例如登录控制台或修改外部变量等都不会发生.
至于共享状态, 这里有一个简单的例子, 即使输入是相同的, 状态仍可以改变函数的输出. 设置一个具有两个函数的代码: 一个将数字加 5, 另一个将数字乘以 5.
- const num = {
- val: 1
- };
- const add5 = () => num.val += 5;
- const multiply5 = () => num.val *= 5;
如果先调用 add5 在调用乘以 5, 则整体结果为 30. 但是如果以相反的方式执行函数并记录结果, 则输出为 10, 与之前结果不一致.
这违背了函数式编程的原理, 因为函数的结果因 Context 调用方法而异. 重新编写上面的代码, 以便结果更易预测:
- const num = {
- val: 1
- };
- const add5 = () => Object.assign({}, num, {val: num.val + 5}); const multiply5 = () => Object.assign({}, num, {val: num.val * 5});
现在, num.val 的值仍然为 1, 无论 Context 调用的方法如何, add5(num)和 multiply5(num)将始终输出相同的结果.
8. 命令式和声明性编程之间有什么区别?
关于命令式编程和声明式编程的区别, 可以以 OOP(面向对象编程)和 FP(函数式编程)为参考.
这两种是描述多种不同编程范式共有特征的概括性术语. FP(函数式编程)是声明性编程的一个范例, 而 OOP(面向对象编程)是命令式编程的一个范例.
从基本的意义层面, 命令式编程关注的是如何做某事. 它以最基本的方式阐明了步骤, 并以 for 和 while 循环, if 和 switch 陈述句等为特征.
- const sumArray = array => {
- let result = 0;
- for (let i = 0; i < array.length; i++) {
- result += array[i]
- };
- return result;
- }
相比之下, 声明性编程关注的是做什么, 它通过依赖表达式将怎样做抽出来. 这通常会产生更简洁的代码, 但是在规模上, 由于透明度低, 调试会更加困难.
这是上述的 sumArray()函数的声明方法.
const sumArray = array => { return array.reduce((x, y) => x + y) };
9. 是什么基于原型的继承?
最后, 要讲到的是基于原型的继承. 面向对象编程有几种不同的类型, JavaScript 使用的是基于原型的继承. 该系统通过使用现有对象作为原型, 允许重复运行.
即使是首次遇到原型这一概念, 使用内置方法时也会遇到原型系统. 例如, 用于操作数组的函数 (如 map,reduce,splice 等) 都是 Array.prototype 对象的方法. 实际上, 数组的每个实例 (使用方括号[] 定义, 或者 - 不常见的 new Array())都继承自 Array.prototype, 这就是为什么 map,reduce 和 spliceare 等方法都默认可用的原因.
几乎所有内置对象都是如此, 例如字符串和布尔运算: 只有少数, 如 Infinity,NaN,null 和 undefined 等没有类或方法.
在原型链的末尾, 能发现 Object.prototype, 几乎 JavaScript 中的每个对象都是 Object 的一个实例. 比如 Array. prototype 和 String. prototype 都继承了 Object.prototype 的类和方法.
要想对使用 prototype syntax 的对象添加类和方法, 只需将对象作为函数启动, 并使用 prototype 关键字添加类和方法:
- function Person() {
- };
- Person.prototype.forename = "John";
- Person.prototype.surname = "Smith";
是否应该覆盖或扩展原型运算?
可以使用与创建扩展 prototypes 同样的方式改变内置运算, 但是大多数开发人员 (以及大多数公司) 不会建议这样做.
如果希望多个对象进行同样的运算, 可以创建一个自定义对象(或定义你自己的 "类" 或 "子类"), 这些对象继承内置原型而不改变原型本身. 如果打算与其他开发人员合作, 他们对 JavaScript 的默认行为有一定的预期, 编辑此默认行为很容易导致出错.
总的来说, 这些问题能够帮助你更好理解 JavaScript, 包括其核心功能和其他鲜为人知的功能 , 并且望能助你为下次的面试做好准备.
来源: http://developer.51cto.com/art/201907/599861.htm