9道JS面试题
发表日期:2019-06-12
这9道题来自于Medium的一篇文章《9 JavaScript Interview Questions》。作者把它分为两部分,第一部分是考察JS中的一些quirks,作者称其为 Curveball Questions,第二部分是一般性问题,Common Questions。
CURVEBALL QUESTIONS
1. 为什么Math.max()小于Math.min()?
Math.max()小于Math.min()?JS中,Math.max() > Math.min()返回false,这看起来不符合常理,但如果仔细分析后,你会发现false这个结果在逻辑上无懈可击。
在没有参数传入的情况下,调用Math.min()和Math.max()分别返回infinity和-infinity。为什么会返回这样的值呢?让我们看一段代码:
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会返回false?
0.1 + 0.2 === 0.3会返回false?简单地讲,这与JS如何精确地用二进制存储浮点数有关。如果你在Chrome的控制台中输入下面的代码,你会看到:
0.1 + 0.2
// 0.30000000000000004
0.1 + 0.2 - 0.2
// 0.10000000000000003
0.1 + 0.7
// 0.7999999999999999如果你没有高精度的计算要求,只需要执行一些简单的计算,那么这是没有问题的。但如果在一些简单的等值比较中,这个问题依然令人头疼。下面是一些解决方案。
固定小数位数。如果你准确的知道你需要的最大精度,比如有关货币的计算,你可以用整型来存储值。例如¥4.99,你可以转换成499来存储和运算,最后在面向用户的展示层,用类似这样的表达式处理,result = (value / 100).toFixed(2),返回一个字符串。
Binary Coded Decimals(二-十进制代码)。一般称为BCD码。这种编码形式使二进制和十进制之间的转换能够快捷进行。采用BCD码,既可保存数值的精确度,又可避免使电脑作浮点运算时所消耗的时间。缺点是每一个十进制数都分别存储在一个字节中,即占用8比特,这在内存利用效率上无疑是低效的。但如果你对精度的要求很苛刻,那么值得权衡。(JS的BCD库)
BCD码可分为有权码和无权码两类:有权BCD码有8421码、2421码、5421码,其中8421码是最常用的;无权BCD码有余3码、格雷码等。
计算机中的BCD码,经常使用的有两种格式,即分离BCD码,组合BCD码。
所谓分离BCD码,即用一个字节的低四位编码表示十进制数的一位,例如数字82的存放格式为: ————1 0 0 0 ———— 0 0 1 0 其中—表示无关值,一般会用0填充。
填充BCD码,是将两位十进制数,存放在一个字节中,比如82的存放格式是10000010
以上整理来源于CSDN
3. 为什么018减去017等于3?
018 - 017返回3,这个问题与类型隐式转换有关。我们先来讨论一下八进制(octal)。
在计算机领域中二进制(binary)与十六进制(hexadecimal)很常用,但事实上,八进制在上世纪50到60年代扮演着十分重要的角色,它被用来缩写二进制,降低成本。之后十六进制便迅速的出现并应用。详见Quora。
在现代计算机领域八进制有什么作用呢?八进制在某些场景中比十六进制更有优势,因为它不需要用字母来计数(不用A-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出于类似的原因,它也用于数字显示器。
回到这个问题本身,在JS中,在任何数字前加0都会被视作八进制数字。但是8这个数字在八进制中并不存在,所以包含8的数字又会被隐式转换为一个常规的十进制数字。这样017是八进制,018是十进制,所以如果用十进制表达018 - 017,就相当于18 - 15。
COMMON QUESTIONS
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 };使用上,它们最重要的区别是函数声明可以被提升,函数表达式不可以。函数声明被JS解释器提升到作用域顶部,所以你可以在任何地方调用函数声明。相对的,你只能顺序调用函数表达式,也就是说,你必须在调用前定义函数表达式。
如今,很多开发者更加偏好函数表达式,有一些原因可以解释:
首要原因是,函数表达式可以很好的书写更加可预测、结构化的代码。当然,函数声明也可以实现结构化的代码。
其次,我们可以用ES6的语法去定义函数表达式,这通常会更加简洁,并且使用let和const可以让我们更好的控制一个变量能否被重新赋值,你将会在下一个问题中深入体会到。
5. var、let和const之间有什么不同?
这个问题在ES6发布后,是一个会经常被问到的面试题。从JS第一个版本开始,var就作为变量声明的关键字,但是它的缺陷导致ES6采用了两个新的关键字来代替:let和const。
这三个关键字对于赋值、变量提升和作用域有不同的处理方式,下面我们分别来看。
赋值。最基本的不同是,let和var可以被重新赋值,而const是不可以的。如果一个变量是不需要改变的,最好声明成const,这会避免一些意外重新赋值的失误。需要注意的是,const是允许变量(常量)变异的,这意味着如果变量(常量)指向的是一个类似数组或对象的引用数据类型,那么你可以改变引用中所保存的值,只是不能修改引用本身。let和var都可以被重新赋值,但是需要明白,let要比var更优。
变量提升。与函数声明与函数表达式提升类似(参见上一个问题),用var声明的变量会被提升到各自作用域的顶部,而用const和let声明的变量虽然有提升,但如果你在声明前访问的话,会触发‘’‘暂时性死区’的错误。(此处原文有一个例子,来证明let在变量提升上的优势可以减少错误发生,但并不是十分恰当,遂略过,后边补充一下暂时性死区的知识)
暂时性死区
关于const和let对变量是否有提升作用,社区争议一直很大,我更倾向于肯定的答案。原因如下:
x = "global";
// 函数作用域
(function() {
x; // undefined
var x = 1;
}());
// 块作用域
{
x; // x is not defined
let/const x = 1;
}很显然,如果let和const不存在变量提升的话,第十行不应该报错,而是应该打印出"global"。
要搞清楚提升的本质,需要理解 JS 变量的创建(create)、初始化(initialize) 和赋值(assign)。
假设有如下代码:
function fn(){
var x = 1
var y = 2
}
fn()在执行 fn 时,会有以下过程(不完全):
进入
fn,为fn创建一个环境。找到
fn中所有用var声明的变量,在这个环境中「创建」这些变量(即x和y)。将这些变量「初始化」为
undefined。开始执行代码
x = 1将x变量「赋值」为1y = 2将y变量「赋值」为2
也就是说 var 声明会在代码执行之前就将创建变量,并将其初始化为 undefined。
这就解释了为什么在 var x = 1 之前 console.log(x) 会得到 undefined。
接下来看 let 声明的「创建、初始化和赋值」过程
假设代码如下:
{
let x = 1
x = 2
}我们只看{} 里面的过程:
找到所有用
let声明的变量,在环境中「创建」这些变量开始执行代码(注意现在还没有初始化)
执行
x = 1,将x「初始化」为1(这并不是一次赋值,如果代码是let x,就将x初始化为undefined)执行
x = 2,对x进行「赋值」
这就解释了为什么在 let x 之前使用 x 会报错:
let x = 'global'
{
console.log(x) // Uncaught ReferenceError: x is not defined
let x = 1
}原因有两个:
console.log(x)中的x指的是下面的x,而不是全局的x执行 log 时
x还没「初始化」,所以不能使用(也就是所谓的暂时性死区)
看到这里,你应该明白了 let 到底有没有提升:
let的「创建」过程被提升了,但是「初始化」没有提升。var的「创建」和「初始化」都被提升了。
最后看 const,其实 const 和 let 只有一个区别,那就是 const 只有「创建」和「初始化」,没有「赋值」过程。(如果只写const foo;,解释器会报错提示Missing initializer in const declaration)
所以,所谓暂时性死区,就是不能在初始化之前,使用变量。 以上暂时性死区部分,整理来源于简书。
var变量存在函数作用域,let和const变量有块级作用域。一般地,大括号{}、函数、条件和循环语句都可以形成块级作用域。通过下面的例子可以更好的说明它们的差异:
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赋值时,不用var、let、const这样的关键字声明,在x还没有被定义的情况下,x = 1就相当于window.x = 1。原作者在一篇文章中讨论过JS中内存管理的问题,其中提到,这样的写法会导致内存泄露。
为了防止这样的错误发生,你可以使用ES5引入的严格模式,通过在文档或某个函数顶部写一句use strict来实现。此时,如果你在赋值时不使用关键字时,你会得到一个错误提示:Uncaught SyntaxError: Unexpected indentifier。
7. 面向对象(OOP)和函数式编程(FP)有什么区别?
JS是一个多范式语言,也就是说,它支持很多种不同的编程风格,包括事件驱动、函数式和面向对象。
在计算机编程中,有很多种编程范式,但函数式编程和面向对象编程是现在最为流行的,而JS对于这两种风格都支持。
面向对象编程(Object-Oriented Programming)
OOP是基于“对象”这个概念的。“对象”是由一系列属性和方法构成的。
JS中有一些内建对象,比如Math、JSON和一些原始数据类型,像String,Array,Number和Boolean。
无论如何,你都会用到一些内建对象中的方法、或者原型和类,实质上,这就是在实践面向对象编程。
函数式编程(Functional Programming)
FP是基于“纯函数”这个概念的。它提倡避免使用共享状态、可变数据和一些副作用。这看起来有一堆术语限定,但其实你有大把的机会在代码中写纯函数。
对于相同的输入,纯函数总会有不变的输出。纯函数不允许有任何副作用,比如在控制台中打印日志,或者修改外部变量的值,这些都超越了返回结果应有的影响。
对于共享状态,下面这个例子展示了在相同输入的条件下,改变了函数的输出。我们写了两个方法,一个是加5,一个是乘以5。
const num = {
val: 1
};
const add5 = () => num.val += 5;
const multiply5 = () => num.val *= 5;如果我们先调用add5,后调用multiply5,那么结果是30。但如果我们以相反的顺序调用函数,那么我们有会得到10。
这与函数式编程的原则相违背,因为函数改变了上下文。下面我们重写了上边的例子,使得结果变得可预测:
const num = {
val: 1
};
const add5 = () => Object.assign({}, num, {val: num.val + 5});
const multiply5 = () => Object.assign({}, num, {val: num.val * 5});现在num.val不再会被改变,而且不管上下文如何,add5()和multiply()将总是返回前后一致的结果。
8. 命令式编程和声明式编程有何不同?
说到声明式(declarative programming)和命令式编程(imperative programming)的区别,我们同样可以思考OOP与FP之间的区别。
这是对不同编程范式中,一些共同特点的总称。函数式编程是声明式风格的典型代表,而面向对象编程则是命令式风格的范例。
基本上,命令式编程关心的是你怎样做某事。它会拼装每一步关键逻辑,处处都是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. 什么是基于原型的继承?
JS通过“prototype”来实现面向对象编程的继承特性。事实上,JS中内建的操作数组的方法map、reduce、splice等等,都是继承自Array.prototype。同样像String、Boolean这样的对象也都有原型,但是像Infinity,NaN,null和undefined这样的值是没有属性和方法的。
是否应该重写或者扩展原型上的行为呢?
JS中几乎所有的对象,在原型链最上游都是Object.prototype。你可以轻易的通过prototype关键字为一个对象添加属性和方法,或者改变内建对象的行为,但是大多数公司反对这样做。
function Person() {};
Person.prototype.forename = "John";
Person.prototype.surname = "Smith";如果你想要让几个对象共享一些相同的行为,那么你可以创建一个基类或者子类,通过它们继承内建对象的原型,而不必改变内建对象本身。在你与其他开发者协作的时候,对于JS默认行为的结果预计,应该是确定的,修改这些默认行为极易引发错误。
然而,值得注意的是,不是所有人都同意禁止扩展内建对象原型的。在这篇文章中,Eich认为,原型的设计,一部分原因就是为了实现扩展。
最后更新于