本文来自于我的微信公众号 —
闪兔网络工作室
:前端面试整理—Javascipt问题,转载请保留链接 ;)
这篇文章紧接前端面试整理—Javascript(一),欢迎继续阅读。
==
和===
的区别是什么?
==
是抽象相等运算符,而===
是严格相等运算符。==
运算符是在进行必要的类型转换后,再比较。===
运算符不会进行类型转换,所以如果两个值不是相同的类型,会直接返回false
。使用==
时,可能发生一些特别的事情,例如:
1 == '1'; // true1 == [1]; // true1 == true; // true0 == ''; // true0 == '0'; // true0 == false; // true
我的建议是从不使用==
运算符,除了方便与null
或undefined
比较时,a == null
如果a
为null
或undefined
将返回true
。
var a = null;console.log(a == null); // trueconsole.log(a == undefined); // true
参考
请解释关于 JavaScript 的同源策略。
同源策略可防止 JavaScript 发起跨域请求。源被定义为URI、主机名和端口号的组合。此策略可防止页面上的恶意脚本通过该页面的文档对象模型,访问另一个网页上的敏感数据。
参考
请使下面的语句生效:
duplicate([1, 2, 3, 4, 5]); // [1,2,3,4,5,1,2,3,4,5]
function duplicate(arr) { return arr.concat(arr); }duplicate([1, 2, 3, 4, 5]); // [1,2,3,4,5,1,2,3,4,5]
请说明三元表达式中“三元”这个词代表什么?
“三元”表示接受三个操作数:判断条件,then
表达式和else
表达式。三元表达式不是 JavaScript 特有的,我不知道这个问题为什么会出现在这里。
参考
什么是"use strict";
?使用它有什么优缺点?
‘use strict’ 是用于对整个脚本或单个函数启用严格模式的语句。严格模式是可选择的一个限制 JavaScript 的变体一种方式 。
优点:
-
无法再意外创建全局变量。
-
会使引起静默失败(silently fail,即:不报错也没有任何效果)的赋值操抛出异常。
-
试图删除不可删除的属性时会抛出异常(之前这种操作不会产生任何效果)。
-
要求函数的参数名唯一。
-
全局作用域下,
this
的值为undefined
。 -
捕获了一些常见的编码错误,并抛出异常。
-
禁用令人困惑或欠佳的功能。
缺点:
-
缺失许多开发人员已经习惯的功能。
-
无法访问
function.caller
和function.arguments
。 -
以不同严格模式编写的脚本合并后可能导致问题。
总的来说,我认为利大于弊,我从来不使用严格模式禁用的功能,因此我推荐使用严格模式。
参考
创建一个循环,从1迭代到100,3
的倍数时输出 “fizz”,5
的倍数时输出 “buzz”,同时为3
和5
的倍数时输出 “fizzbuzz”。
来自 Paul Irish的 FizzBuzz。
for (let i = 1; i <= 100; i++) { let f = i % 3 == 0, b = i % 5 == 0; console.log(f ? (b ? 'FizzBuzz' : 'Fizz') : b ? 'Buzz' : i); }
我不建议你在面试时写上面的代码。只要写得清晰即可。关于更多千奇百怪的 FizzBuzz 实现,请查看下面的参考链接。
参考
为什么不要使用全局作用域?
每个脚本都可以访问全局作用域,如果人人都使用全局命名空间来定义自己的变量,肯定会发生冲突。使用模块模式(IIFE)将变量封装在本地命名空间中。
为什么要使用load
事件?这个事件有什么缺点吗?你知道一些代替方案吗,为什么使用它们?
在文档装载完成后会触发load
事件。此时,在文档中的所有对象都在DOM中,所有图像、脚本、链接和子框架都完成了加载。
DOM 事件DOMContentLoaded
将在页面的DOM构建完成后触发,但不要等待其他资源完成加载。如果在初始化之前不需要装入整个页面,这个事件是使用首选。
TODO.
参考
请解释单页应用是什么,如何使其对SEO友好。
以下摘自 Grab Front End Guide,碰巧的是,这正是我自己写的!
现如今,Web开发人员将他们构建的产品称为Web应用,而不是网站。虽然这两个术语之间没有严格的区别,但网络应用往往具有高度的交互性和动态性,允许用户执行操作并接收他们的操作响应。在过去,浏览器从服务器接收 HTML并渲染。当用户导航到其它 URL 时,需要整页刷新,服务器会为新页面发送新的 HTML。这被称为服务器端渲染。
然而,在现代的 SPA 中,客户端渲染取而代之。浏览器从服务器加载初始页面、整个应用程序所需的脚本(框架、库、应用代码)和样式表。当用户导航到其他页面时,不会触发页面刷新。该页面的URL通过 HTML5 History API 进行更新。浏览器通过 AJAX 请求向服务器检索新页面所需的数据(通常采用JSON格式)。然后,SPA 通过 JavaScript来动态更新页面,这些 JavaScript 在初始页面加载时已经下载。这种模式类似于原生移动应用的工作方式。
好处:
-
用户感知响应更快,用户切换页面时,不再看到因页面刷新而导致的白屏。
-
对服务器进行的 HTTP 请求减少,因为对于每个页面加载,不必再次下载相同的资源。
-
客户端和服务器之间的关注点分离。可以为不同平台(例如手机、聊天机器人、智能手表)建立新的客户端,而无需修改服务器代码。只要 API 没有修改,可以单独修改客户端和服务器上的代码。
坏处:
-
由于加载了多个页面所需的框架、应用代码和资源,导致初始页面加载时间较长。
-
服务器还需要进行额外的工作,需要将所有请求路由配置到单个入口点,然后由客户端接管路由。
-
SPA 依赖于 JavaScript 来呈现内容,但并非所有搜索引擎都在抓取过程中执行 JavaScript,他们可能会在你的页面上看到空的内容。这无意中损害了应用的搜索引擎优化(SEO)。然而,当你构建应用时,大多数情况下,搜索引擎优化并不是最重要的因素,因为并非所有内容都需要通过搜索引擎进行索引。为了解决这个问题,可以在服务器端渲染你的应用,或者使用诸如 Prerender 的服务来“在浏览器中呈现你的 javascript,保存静态 HTML,并将其返回给爬虫”。
参考
-
https://github.com/grab/front-end-guide#single-page-apps-spas
-
http://stackoverflow.com/questions/21862054/single-page-app-advantages-and-disadvantages
-
http://blog.isquaredsoftware.com/presentations/2016-10-revolution-of-web-dev/
-
https://medium.freecodecamp.com/heres-why-client-side-rendering-won-46a349fadb52
你对 Promises 及其 polyfill 的掌握程度如何?
掌握它的工作原理。Promise
是一个可能在未来某个时间产生结果的对象:操作成功的结果或失败的原因(例如发生网络错误)。 Promise
可能处于以下三种状态之一:fulfilled、rejected 或 pending。 用户可以对Promise
添加回调函数来处理操作成功的结果或失败的原因。
一些常见的 polyfill 是$.deferred
、Q 和 Bluebird,但不是所有的 polyfill 都符合规范。ES2015 支持 Promises,现在通常不需要使用 polyfills。
参考
Promise
代替回调函数有什么优缺点?
优点:
-
避免可读性极差的回调地狱。
-
使用
.then()
编写的顺序异步代码,既简单又易读。 -
使用
Promise.all()
编写并行异步代码变得很容易。
缺点:
-
轻微地增加了代码的复杂度(这点存在争议)。
-
在不支持 ES2015 的旧版浏览器中,需要引入 polyfill 才能使用。
用转译成 JavaScript 的语言写 JavaScript 有什么优缺点?
Some examples of languages that compile to JavaScript include CoffeeScript, Elm, ClojureScript, PureScript andTypeScript. 这些是转译成 JavaScript 的语言,包括 CoffeeScript、Elm、ClojureScript、PureScript 和 TypeScript。
优点:
-
修复了 JavaScript 中的一些长期问题,并摒弃了 JavaScript 不好的做法。
-
在 JavaScript 的基础上提供一些语法糖,使我们能够编写更短的代码,我认为 ES5 缺乏语法糖的支持,但ES2015 非常好。
-
对于需要长时间维护的大型项目,静态类型非常好用(针对 TypeScript)。
缺点:
-
由于浏览器只运行JavaScript,所以需要构建、编译过程,在将代码提供给浏览器之前,需要将代码转译为JavaScript。
-
如果 source map 不能很好地映射到预编译的源代码,调试会很痛苦。
-
大多数开发人员不熟悉这些语言,需要学习它。如果将其用于项目,会增加团队成本。
-
社区比较小(取决于语言),这意味着资源、教程、图书和工具难以找到。
-
可能缺乏IDE(编辑器)的支持。
-
这些语言将始终落后于最新的 JavaScript 标准。
-
开发人员应该清楚代码正在被编译到什么地方——因为这是实际运行的内容,是最重要的。
实际上,ES2015 已经大大改进了 JavaScript,编写体验很好。我现在还没有真正看到对 CoffeeScript 的需求。
参考
你使用什么工具和技巧调试 JavaScript 代码?
-
React 和 Redux
-
React Devtools
-
Redux Devtools
-
Vue
-
Vue Devtools
-
JavaScript
-
Chrome Devtools
-
debugger
声明 -
使用万金油
console.log
进行调试
参考
你使用什么语句遍历对象的属性和数组的元素?
对象:
-
for
循环:for (var property in obj) { console.log(property); }
。但是,这还会遍历到它的继承属性,在使用之前,你需要加入obj.hasOwnProperty(property)
检查。 -
Object.keys()
:Object.keys(obj).forEach(function (property) { ... })
。Object.keys()
方法会返回一个由一个给定对象的自身可枚举属性组成的数组。 -
Object.getOwnPropertyNames()
:Object.getOwnPropertyNames(obj).forEach(function(property) { ... })
。Object.getOwnPropertyNames()
方法返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性但不包括Symbol值作为名称的属性)组成的数组。
数组:
-
for
loops:for (var i = 0; i < arr.length; i++)
。这里的常见错误是var
是函数作用域而不是块级作用域,大多数时候你想要迭代变量在块级作用域中。ES2015 引入了具有块级作用域的let
,建议使用它。所以就变成了:for (let i = 0; i < arr.length; i++)
。 -
forEach
:arr.forEach(function (el, index) { ... })
。这个语句结构有时会更精简,因为如果你所需要的只是数组元素,你不必使用index
。还有every
和some
方法可以让你提前终止遍历。
大多数情况下,我更喜欢.forEach
方法,但这取决于你想要做什么。for
循环有更强的灵活性,比如使用break
提前终止循环,或者递增步数大于一。
请解释可变对象和不可变对象之间的区别。
-
什么是 JavaScript 中的不可变对象的例子?
-
不变性有什么优点和缺点?
-
你如何在自己的代码中实现不变性?
可变对象 在创建之后是可以被改变的。
不可变对象 在创建之后是不可以被改变的。
-
在
JavaScript
中,string
和number
从设计之初就是不可变(Immutable)。 -
不可变 其实是保持一个对象状态不变,这样做的好处是使得开发更加简单,可回溯,测试友好,减少了任何可能的副作用。但是,每当你想添加点东西到一个不可变(Immutable)对象里时,它一定是先拷贝已存在的值到新实例里,然后再给新实例添加内容,最后返回新实例。相比可变对象,这势必会有更多内存、计算量消耗。
-
比如:构造一个纯函数
const student1 = { school: "Baidu", name: 'HOU Ce', birthdate: '1995-12-15', }const changeStudent = (student, newName, newBday) => { return { ...student, // 使用解构 name: newName, // 覆盖name属性 birthdate: newBday // 覆盖birthdate属性 } }const student2 = changeStudent(student1, 'YAN Haijing', '1990-11-10');// both students will have the name propertiesconsole.log(student1, student2);// Object {school: "Baidu", name: "HOU Ce", birthdate: "1995-12-15"}// Object {school: "Baidu", name: "YAN Haijing", birthdate: "1990-11-10"}
参考
请解释同步和异步函数之间的区别。
同步函数阻塞,而异步函数不阻塞。在同步函数中,语句完成后,下一句才执行。在这种情况下,程序可以按照语句的顺序进行精确评估,如果其中一个语句需要很长时间,程序的执行会停滞很长时间。
异步函数通常接受回调作为参数,在调用异步函数后立即继续执行下一行。回调函数仅在异步操作完成且调用堆栈为空时调用。诸如从 Web 服务器加载数据或查询数据库等重负载操作应该异步完成,以便主线程可以继续执行其他操作,而不会出现一直阻塞,直到费时操作完成的情况(在浏览器中,界面会卡住)。
什么是事件循环?调用堆栈和任务队列之间有什么区别?
事件循环是一个单线程循环,用于监视调用堆栈并检查是否有工作即将在任务队列中完成。如果调用堆栈为空并且任务队列中有回调函数,则将回调函数出队并推送到调用堆栈中执行。
如果你没有看过 Philip Robert 关于事件循环的演讲,你应该看一下。这是观看次数最多的 JavaScript 相关视频之一。
参考
请解释function foo() {}
和var foo = function() {}
之间foo
的用法上的区别。
前者是函数声明,后者是函数表达式。关键的区别在于函数声明会使函数体提升(具有与变量相同的提升行为),但函数表达式的函数体不能。有关变量提升的更多解释,请参阅上面关于变量提升的问题。如果你试图在定义函数表达式之前调用它,你会得到一个Uncaught TypeError: XXX is not a function
的错误。
函数声明
foo(); // 'FOOOOO'function foo() { console.log('FOOOOO'); }
函数表达式
foo(); // Uncaught TypeError: foo is not a functionvar foo = function() { console.log('FOOOOO'); };
参考
使用let
、var
和const
创建变量有什么区别?
用var
声明的变量的作用域是它当前的执行上下文,它可以是嵌套的函数,也可以是声明在任何函数外的变量。let
和const
是块级作用域,意味着它们只能在最近的一组花括号(function、if-else 代码块或 for 循环中)中访问。
function foo() { // 所有变量在函数中都可访问 var bar = 'bar'; let baz = 'baz'; const qux = 'qux'; console.log(bar); // bar console.log(baz); // baz console.log(qux); // qux}console.log(bar); // ReferenceError: bar is not definedconsole.log(baz); // ReferenceError: baz is not definedconsole.log(qux); // ReferenceError: qux is not definedif (true) { var bar = 'bar'; let baz = 'baz'; const qux = 'qux'; }// 用 var 声明的变量在函数作用域上都可访问console.log(bar); // bar// let 和 const 定义的变量在它们被定义的语句块之外不可访问console.log(baz); // ReferenceError: baz is not definedconsole.log(qux); // ReferenceError: qux is not defined
var
会使变量提升,这意味着变量可以在声明之前使用。let
和const
不会使变量提升,提前使用会报错。
console.log(foo); // undefinedvar foo = 'foo';console.log(baz); // ReferenceError: can't access lexical declaration 'baz' before initializationlet baz = 'baz';console.log(bar); // ReferenceError: can't access lexical declaration 'bar' before initializationconst bar = 'bar';
用var
重复声明不会报错,但let
和const
会。
var foo = 'foo';var foo = 'bar';console.log(foo); // "bar"let baz = 'baz';let baz = 'qux'; // Uncaught SyntaxError: Identifier 'baz' has already been declared
let
和const
的区别在于:let
允许多次赋值,而const
只允许一次。
// 这样不会报错。let foo = 'foo'; foo = 'bar';// 这样会报错。const baz = 'baz'; baz = 'qux';
参考
ES6 的类和 ES5 的构造函数有什么区别?
TODO
你能给出一个使用箭头函数的例子吗,箭头函数与其他函数有什么不同?
TODO
在构造函数中使用箭头函数有什么好处?
TODO
高阶函数(higher-order)的定义是什么?
高阶函数是将一个或多个函数作为参数的函数,它用于数据处理,也可能将函数作为返回结果。高阶函数是为了抽象一些重复执行的操作。一个典型的例子是map
,它将一个数组和一个函数作为参数。map
使用这个函数来转换数组中的每个元素,并返回一个包含转换后元素的新数组。JavaScript 中的其他常见示例是forEach
、filter
和reduce
。高阶函数不仅需要操作数组的时候会用到,还有许多函数返回新函数的用例。Function.prototype.bind
就是一个例子。
Map 示例:
假设我们有一个由名字组成的数组,我们需要将每个字符转换为大写字母。
const names = ['irish', 'daisy', 'anna'];
不使用高阶函数的方法是这样:
const transformNamesToUppercase = function(names) { const results = []; for (let i = 0; i < names.length; i++) { results.push(names[i].toUpperCase()); } return results; };transformNamesToUppercase(names); // ['IRISH', 'DAISY', 'ANNA']
使用.map(transformerFn)
使代码更简明
const transformNamesToUppercase = function(names) { return names.map(name => name.toUpperCase()); };transformNamesToUppercase(names); // ['IRISH', 'DAISY', 'ANNA']
参考
请给出一个解构(destructuring)对象或数组的例子。
解构是 ES6 中新功能,它提供了一种简洁方便的方法来提取对象或数组的值,并将它们放入不同的变量中。
数组解构
// 变量赋值const foo = ['one', 'two', 'three'];const [one, two, three] = foo;console.log(one); // "one"console.log(two); // "two"console.log(three); // "three"
// 变量交换let a = 1;let b = 3; [a, b] = [b, a];console.log(a); // 3console.log(b); // 1
对象解构
// 变量赋值const o = { p: 42, q: true };const { p, q } = o;console.log(p); // 42console.log(q); // true
参考
ES6 的模板字符串为生成字符串提供了很大的灵活性,你可以举个例子吗?
模板字面量(Template literals) 是允许嵌入表达式的字符串字面量。你可以使用多行字符串和字符串插值功能。
语法
`string text``string text line 1 string text line 2``string text ${expression} string text`tag `string text ${expression} string text`
示例
console.log(`string text line 1string text line 2`);// "string text line 1// string text line 2"var a = 5;var b = 10;console.log(`Fifteen is ${a + b} and\nnot ${2 * a + b}.`);// "Fifteen is 15 and// not 20."
//show函数采用rest参数的写法如下:let name = '张三', age = 20, message = show`我来给大家介绍:${name}的年龄是${age}.`;function show(stringArr,...values){let output ="";let index = 0 for(;index
参考
你能举出一个柯里化函数(curry function)的例子吗?它有哪些好处?
柯里化(currying)是一种模式,其中具有多个参数的函数被分解为多个函数,当被串联调用时,将一次一个地累积所有需要的参数。这种技术帮助编写函数式风格的代码,使代码更易读、紧凑。值得注意的是,对于需要被curry的函数,它需要从一个函数开始,然后分解成一系列函数,每个函数都需要一个参数。
function curry(fn) { if (fn.length === 0) { return fn; } function _curried(depth, args) { return function(newArgument) { if (depth - 1 === 0) { return fn(...args, newArgument); } return _curried(depth - 1, [...args, newArgument]); }; } return _curried(fn.length, []); }function add(a, b) { return a + b; }var curriedAdd = curry(add);var addFive = curriedAdd(5);var result = [0, 1, 2, 3, 4, 5].map(addFive); // [5, 6, 7, 8, 9, 10]
参考
使用扩展运算符(spread)的好处是什么,它与使用剩余参数语句(rest)有什么区别?
在函数泛型编码时,ES6 的扩展运算符非常有用,因为我们可以轻松创建数组和对象的拷贝,而无需使用Object.create
、slice
或其他函数库。这个语言特性在 Redux 和 rx.js 的项目中经常用到。
function putDookieInAnyArray(arr) { return [...arr, 'dookie']; }const result = putDookieInAnyArray(['I', 'really', "don't", 'like']); // ["I", "really", "don't", "like", "dookie"]const person = { name: 'Todd', age: 29, };const copyOfTodd = { ...person };
ES6 的剩余参数语句提供了一个简写,允许我们将不定数量的参数表示为一个数组。它就像是扩展运算符语法的反面,将数据收集到数组中,而不是解构数组。剩余参数语句在函数参数、数组和对象的解构赋值中有很大作用。
function addFiveToABunchOfNumbers(...numbers) { return numbers.map(x => x + 5); }const result = addFiveToABunchOfNumbers(4, 5, 6, 7, 8, 9, 10); // [9, 10, 11, 12, 13, 14, 15]const [a, b, ...rest] = [1, 2, 3, 4]; // a: 1, b: 2, rest: [3, 4]const { e, f, ...others } = { e: 1, f: 2, g: 3, h: 4, }; // e: 1, f: 2, others: { g: 3, h: 4 }
参考
如何在文件之间共用代码?
这取决于执行 JavaScript 的环境。
在客户端(浏览器环境)上,只要变量或函数在全局作用域(window
)中声明,所有脚本都可以引用它们。或者,通过 RequireJS 采用异步模块定义(AMD)以获得更多模块化方法。
在服务器(Node.js)上,常用的方法是使用 CommonJS。每个文件都被视为一个模块,可以通过将它们附加到module.exports
对象来导出变量和函数。
ES2015 定义了一个模块语法,旨在替换 AMD 和 CommonJS。 这最终将在浏览器和 Node 环境中得到支持。
参考
什么情况下会用到静态类成员?
静态类成员(属性或方法)不绑定到某个类的特定实例,不管哪个实例引用它,都具有相同的值。静态属性通常是配置变量,而静态方法通常是纯粹的实用函数,不依赖于实例的状态。