图片来源:Irvan Smith / Unsplash
人们认为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
如果是简单的等式,对准确度没有要求,这不太可能产生问题。但是如果需要测试相等性,即使是简单地应用也会导致令人头疼的问题。解决这些问题,有以下几种方案。
Fixed Point固定点
例如,如果知道所需的最大精度(例如,如果正在处理货币),则可以使用整数类型来存储该值。因此,可以存储499而非4.99美元,并在此基础上执行任何等式,然后可以使用类似result =(value / 100).toFixed(2)的表达式将结果显示给最终用户,该表达式返回一个字符串。
BCD代码
如果精度非常重要,另一种方法是使用二进制编码的十进制(BCD)格式,可以使用BCD库(https://formats.kaitai.io/bcd/javascript.html)访问JavaScript。每个十进制值分别存储在一个字节(8位)中。鉴于一个字节可以存储16个单独值,而该系统仅使用0-9位,所以这种方法效率低下。但是,如果十分注重精确度,采用何种方法都值得考量。
3. 为什么018减017等于3?
018-017返回3实际是静默类型转换的结果。这种情况,讨论的是八进制数。
八进制数简介
你或许知道计算中使用二进制(base-2)和十六进制(base-16)数字系统,但是八进制(base-8)在计算机历史中的地位也举足亲重:在20世纪50年代后期和 20世纪60年代间,八进制被用于简化二进制,削减高昂的制造系统中的材料成本。
不久以后Hexadecimal(十六进制)开始登上历史舞台:
1965年发布的IBM360迈出了从八进制到十六进制的决定性一步。我们这些习惯八进制的人对这一举措感到震惊!沃恩·普拉特(Vaughan Pratt)
如今的八进制数
但在现代编程语言中,八进制又有何作用呢?针对某些案例,八进制比十六进制更具优势,因为它不需要任何非数字(使用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
出于相似的原由,八进制也用于数字显示器。
回到问题本身
在JavaScript中,前缀0将所有数字转换为八进制。但是,八进制中不使用数字8,任何包含8的数字都将自动转换为常规十进制数。
因此,018-017实际上等同于十进制表达式:18-15,因为017使用八进制而018使用十进制。
图片来源:pexels.com/@divinetechygirl
本节中,将介绍面试中一些更加常见的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。
这三个关键字具有不同的分配,提升和域 - 因此我们将单独讨论。
i) 分配
最基本的区别是let和var可以重新分配,而const则不能。这使得const成为不变变量的最佳选择,并且它将防止诸如意外重新分配之类的失误。注意,当变量表示数组或对象时,const确实允许变量改变,只是无法重新分配变量本身。
Let 和var都可重新分配,但是正如以下几点应该明确的那样,如果不是所有情况都要求更改变量,多数选择中,let具有优于var的显著优势。
ii)提升
与函数声明和表达式(如上所述)之间的差异类似,使用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()。
iii) 域
虽然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两种都可执行。
面向对象编程
OOP以“对象”这一概念为基础的数据结构,包含数据字段(JavaScript称为类)和程序(JavaScript中的方法)。
一些JavaScript的内置对象包括Math(用于random,max和sin等方法),JSON(用于解析JSON数据)和原始数据类型,如String,Array,Number和Boolean。
无论何时采用的内置方法,原型或类,本质上都在使用面向对象编程。
函数编程
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) };
图片来源:pexels.com/@rawpixel
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,包括其核心功能和其他鲜为人知的功能 ,并且望能助你为下次的面试做好准备。