人们认为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是window.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,包括其核心功能和其他鲜为人知的功能 ,并且望能助你为下次的面试做好准备。