本文来自于我的微信公众号 — 闪兔网络工作室:前端面试整理—Javascipt问题,转载请保留链接 ;)
本章节是前端开发者面试问题 - JS 部分的参考答案。 欢迎提出建议和指正!
-
请解释事件委托(event delegation)。
-
请简述Javascript中的this。
-
请解释原型继承(prototypal inheritance)的工作原理。
-
说说你对 AMD 和 CommonJS 的了解。
-
请解释下面代码为什么不能用作 IIFE:function foo(){ }();,需要作出哪些修改才能使其成为 IIFE?
-
null、undefined和未声明变量之间有什么区别?如何检查判断这些状态值?
-
什么是闭包(closure),为什么使用闭包?
-
请说明.forEach循环和.map()循环的主要区别,它们分别在什么情况下使用?
-
匿名函数的典型应用场景是什么?
-
你如何组织自己的代码?(使用模块模式(module pattern)还是经典继承(classical inheritance)?)
-
宿主对象(host objects)和原生对象(native objects)的区别是什么?
-
下列语句有什么区别:function Person(){}、var person = Person()和var person = newPerson()?
-
.call和.apply有什么区别?
-
请说明Function.prototype.bind的用法。
-
什么时候会用到document.write()?
-
功能检测(feature detection)、功能推断(feature inference)和使用 UA 字符串之间有什么区别?
-
请尽可能详细地解释 Ajax。
-
使用Ajax的优缺点分别是什么?
-
请说明 JSONP 的工作原理,它为什么不是真正的 Ajax?
-
你使用过 Javascript 模板吗?用过什么相关的库?
-
请解释变量提升(hosting)。
-
请描述事件冒泡。
-
“attribute” 和 “property” 之间有什么区别?
-
为什么扩展 Javascript 内置对象是不好的做法?
-
document 中的load事件和DOMContentLoaded事件之间的区别是什么?
-
==和===的区别是什么?
-
请解释关于 Javascript 的同源策略。
-
请使下面的语句生效:
-
请说明三元表达式中“三元”这个词代表什么?
-
什么是"use strict";?使用它有什么优缺点?
-
创建一个循环,从1迭代到100,3的倍数时输出 “fizz”,5的倍数时输出 “buzz”,同时为3和5的倍数时输出"fizzbuzz"。
-
为什么不要使用全局作用域?
-
为什么要使用load事件?这个事件有什么缺点吗?你知道一些代替方案吗,为什么使用它们?
-
请解释单页应用是什么,如何使其对SEO友好。
-
你对 Promises 及其 polyfill 的掌握程度如何?
-
Promise代替回调函数有什么优缺点?
-
用转译成 Javascript 的语言写 Javascript 有什么优缺点?
-
你使用什么工具和技巧调试 Javascript 代码?
-
你使用什么语句遍历对象的属性和数组的元素?
-
请解释可变对象和不可变对象之间的区别。
-
请解释同步和异步函数之间的区别。
-
什么是事件循环?调用堆栈和任务队列之间有什么区别?
-
请解释function foo() {}和var foo = function() {}之间foo的用法上的区别。
-
使用let、var和const创建变量有什么区别?
-
ES6 的类和 ES5 的构造函数有什么区别?
-
你能给出一个使用箭头函数的例子吗,箭头函数与其他函数有什么不同?
-
在构造函数中使用箭头函数有什么好处?
-
高阶函数(higher-order)的定义是什么?
-
请给出一个解构(destructuring)对象或数组的例子。
-
ES6 的模板字符串为生成字符串提供了很大的灵活性,你可以举个例子吗?
-
你能举出一个柯里化函数(curry function)的例子吗?它有哪些好处?
-
使用扩展运算符(spread)的好处是什么,它与使用剩余参数语句(rest)有什么区别?
-
如何在文件之间共用代码?
-
什么情况下会用到静态类成员?
事件委托是将事件监听器添加到父元素,而不是每个子元素单独设置事件监听器。当触发子元素时,事件会冒泡到父元素,监听器就会触发。这种技术的好处是:
-
内存占用减少,因为只需要一个父元素的事件处理程序,而不必为每个后代都添加事件处理程序。
-
无需从已删除的元素中解绑处理程序,也无需将处理程序绑定到新元素上。
-
https://davidwalsh.name/event-delegate
-
https://stackoverflow.com/questions/1687296/what-is-dom-event-delegation
JS 中的this是一个相对复杂的概念,不是简单几句能解释清楚的。粗略地讲,函数的调用方式决定了this的值。我阅读了网上很多关于this的文章,Arnav Aggrawal 写的比较清楚。this取值符合以下规则:
-
在调用函数时使用new关键字,函数内的this是一个全新的对象。
-
如果apply、call或bind方法用于调用、创建一个函数,函数内的 this 就是作为参数传入这些方法的对象。
-
当函数作为对象里的方法被调用时,函数内的this是调用该函数的对象。比如当obj.method()被调用时,函数内的this将绑定到obj对象。
-
如果调用函数不符合上述规则,那么this的值指向全局对象(global object)。浏览器环境下this的值指向window对象,但是在严格模式下('use strict'),this的值为undefined。
-
如果符合上述多个规则,则较高的规则(1号最高,4号最低)将决定this的值。
-
如果该函数是 ES2015 中的箭头函数,将忽略上面的所有规则,this被设置为它被创建时的上下文。
想获得更深入的解释,请查看他在 Medium 上的文章。
参考-
https://codeburst.io/the-simple-rules-to-this-in-javascript-35d97f31bde3
-
https://stackoverflow.com/a/3127440/1751946
这是一个非常常见的 Javascript 问题。所有 JS 对象都有一个prototype属性,指向它的原型对象。当试图访问一个对象的属性时,如果没有在该对象上找到,它还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。这种行为是在模拟经典的继承,但是与其说是继承,不如说是委托(delegation)。
参考-
https://www.quora.com/What-is-prototypal-inheritance/answer/Kyle-Simpson
-
https://davidwalsh.name/javascript-objects
它们都是实现模块体系的方式,直到 ES2015 出现之前,Javascript 一直没有模块体系。CommonJS 是同步的,而AMD(Asynchronous Module Definition)从全称中可以明显看出是异步的。CommonJS 的设计是为服务器端开发考虑的,而 AMD 支持异步加载模块,更适合浏览器。
我发现 AMD 的语法非常冗长,CommonJS 更接近其他语言 import 声明语句的用法习惯。大多数情况下,我认为AMD 没有使用的必要,因为如果把所有 Javascript 都捆绑进一个文件中,将无法得到异步加载的好处。此外,CommonJS 语法上更接近 Node 编写模块的风格,在前后端都使用 Javascript 开发之间进行切换时,语境的切换开销较小。
我很高兴看到 ES2015 的模块加载方案同时支持同步和异步,我们终于可以只使用一种方案了。虽然它尚未在浏览器和Node 中完全推出,但是我们可以使用代码转换工具进行转换。
参考-
https://auth0.com/blog/javascript-module-systems-showdown/
-
https://stackoverflow.com/questions/16521471/relation-between-commonjs-amd-and-requirejs
IIFE(Immediately Invoked Function expressions)代表立即执行函数。 Javascript 解析器将 function foo(){ }();解析成function foo(){ }和();。其中,前者是函数声明;后者(一对括号)是试图调用一个函数,却没有指定名称,因此它会抛出Uncaught SyntaxError: Unexpected token )的错误。
修改方法是:再添加一对括号,形式上有两种:(function foo(){ })()和(function foo(){ }())。以上函数不会暴露到全局作用域,如果不需要在函数内部引用自身,可以省略函数的名称。
你可能会用到 void 操作符:void function foo(){ }();。但是,这种做法是有问题的。表达式的值是undefined,所以如果你的 IIFE 有返回值,不要用这种做法。例如:
const foo = void function bar() { return 'foo'; }();console.log(foo); // undefined
参考
-
http://lucybain.com/blog/2014/immediately-invoked-function-expression/
-
https://developer.mozilla.org/en-US/docs/Web/Javascript/Reference/Operators/void
当你没有提前使用var、let或const声明变量,就为一个变量赋值时,该变量是未声明变量(undeclaredvariables)。未声明变量会脱离当前作用域,成为全局作用域下定义的变量。在严格模式下,给未声明的变量赋值,会抛出ReferenceError错误。和使用全局变量一样,使用未声明变量也是非常不好的做法,应当尽可能避免。要检查判断它们,需要将用到它们的代码放在try/catch语句中。
function foo() { x = 1; // 在严格模式下,抛出 ReferenceError 错误}foo();console.log(x); // 1
当一个变量已经声明,但没有赋值时,该变量的值是undefined。如果一个函数的执行结果被赋值给一个变量,但是这个函数却没有返回任何值,那么该变量的值是undefined。要检查它,需要使用严格相等(===);或者使用typeof,它会返回'undefined'字符串。请注意,不能使用非严格相等(==)来检查,因为如果变量值为null,使用非严格相等也会返回true。
var foo;console.log(foo); // undefinedconsole.log(foo === undefined); // trueconsole.log(typeof foo === 'undefined'); // trueconsole.log(foo == null); // true. 错误,不要使用非严格相等!function bar() {}var baz = bar();console.log(baz); // undefined
null只能被显式赋值给变量。它表示空值,与被显式赋值 undefined 的意义不同。要检查判断null值,需要使用严格相等运算符。请注意,和前面一样,不能使用非严格相等(==)来检查,因为如果变量值为undefined,使用非严格相等也会返回true。
var foo = null;console.log(foo === null); // trueconsole.log(foo == undefined); // true. 错误,不要使用非严格相等!
作为一种个人习惯,我从不使用未声明变量。如果定义了暂时没有用到的变量,我会在声明后明确地给它们赋值为null。
参考-
https://stackoverflow.com/questions/15985875/effect-of-declared-and-undeclared-variables
-
https://developer.mozilla.org/en/docs/Web/Javascript/Reference/Global_Objects/undefined
闭包是函数和声明该函数的词法环境的组合。词法作用域中使用的域,是变量在代码中声明的位置所决定的。闭包是即使被外部函数返回,依然可以访问到外部(封闭)函数作用域的函数。
为什么使用闭包?
-
利用闭包实现数据私有化或模拟私有方法。这个方式也称为模块模式(module pattern)。
-
部分参数函数(partial applications)柯里化(currying).
-
https://developer.mozilla.org/en-US/docs/Web/Javascript/Closures
-
https://medium.com/javascript-scene/master-the-javascript-interview-what-is-a-closure-b2f0d2152b36
为了理解两者的区别,我们看看它们分别是做什么的。
forEach
-
遍历数组中的元素。
-
为每个元素执行回调。
-
无返回值。
const a = [1, 2, 3];const doubled = a.forEach((num, index) => { // 执行与 num、index 相关的代码});// doubled = undefined
map
-
遍历数组中的元素
-
通过对每个元素调用函数,将每个元素“映射(map)”到一个新元素,从而创建一个新数组。
const a = [1, 2, 3];const doubled = a.map(num => { return num * 2; });// doubled = [2, 4, 6]
.forEach和.map()的主要区别在于.map()返回一个新的数组。如果你想得到一个结果,但不想改变原始数组,用.map()。如果你只需要在数组上做迭代修改,用forEach。
参考- https://codeburst.io/javascript-map-vs-foreach-f38111822c0f
匿名函数可以在IIFE中使用,来封装局部作用域内的代码,以便其声明的变量不会暴露到全局作用域。
(function() { // 一些代码。})();
匿名函数可以作为只用一次,不需要在其他地方使用的回调函数。当处理函数在调用它们的程序内部被定义时,代码具有更好地自闭性和可读性,可以省去寻找该处理函数的函数体位置的麻烦。
setTimeout(function() { console.log('Hello world!'); }, 1000);
匿名函数可以用于函数式编程或 Lodash(类似于回调函数)。
const arr = [1, 2, 3];const double = arr.map(function(el) { return el * 2; });console.log(double); // [2, 4, 6]
参考
-
https://www.quora.com/What-is-a-typical-usecase-for-anonymous-functions
-
https://stackoverflow.com/questions/10273185/what-are-the-benefits-to-using-anonymous-functions-instead-of-named-functions-fo
我以前使用 Backbone 组织我的模型(model),Backbone 鼓励采用面向对象的方法——创建 Backbone 模型,并为其添加方法。
模块模式仍然是很好的方式,但是现在我使用基于 React/Redux 的 Flux 体系结构,它鼓励使用单向函数编程的方法。我用普通对象(plain object)表示我的 app 模型,编写实用纯函数去操作这些对象。使用动作(actions)和化简器(reducers)来处理状态,就像其他 Redux 应用一样。
我尽可能避免使用经典继承。如果非要这么做,我会坚持这些原则。
宿主对象(host objects)和原生对象(native objects)的区别是什么?原生对象是由 ECMAscript 规范定义的 Javascript 内置对象,比如String、Math、RegExp、Object、Function等等。
宿主对象是由运行时环境(浏览器或 Node)提供,比如window、XMLHTTPRequest等等。
参考- https://stackoverflow.com/questions/7614317/what-is-the-difference-between-native-objects-and-host-objects
这个问题问得很含糊。我猜这是在考察 Javascript 中的构造函数(constructor)。从技术上讲,functionPerson(){}只是一个普通的函数声明。使用 PascalCase 方式命名函数作为构造函数,是一个惯例。
var person = Person()将Person以普通函数调用,而不是构造函数。如果该函数是用作构造函数的,那么这种调用方式是一种常见错误。通常情况下,构造函数不会返回任何东西,因此,像普通函数一样调用构造函数,只会返回undefined赋给用作实例的变量。
var person = new Person()使用new操作符,创建Person对象的实例,该实例继承自Person.prototype。另外一种方式是使用Object.create,例如:Object.create(Person.prototype)`。
function Person(name) { this.name = name; }var person = Person('John');console.log(person); // undefinedconsole.log(person.name); // Uncaught TypeError: Cannot read property 'name' of undefinedvar person = new Person('John');console.log(person); // Person { name: "John" }console.log(person.name); // "john"
参考
- https://developer.mozilla.org/en-US/docs/Web/Javascript/Reference/Operators/new
.call和.apply都用于调用函数,第一个参数将用作函数内this的值。然而,.call接受逗号分隔的参数作为后面的参数,而.apply接受一个参数数组作为后面的参数。一个简单的记忆方法是,从call中的 C 联想到逗号分隔(comma-separated),从apply中的 A 联想到数组(array)。
function add(a, b) { return a + b; }console.log(add.call(null, 1, 2)); // 3console.log(add.apply(null, [1, 2])); // 3
请说明Function.prototype.bind的用法。
摘自MDN:
bind()方法创建一个新的函数, 当被调用时,将其this关键字设置为提供的值,在调用新函数时,在任何提供之前提供一个给定的参数序列。
根据我的经验,将this的值绑定到想要传递给其他函数的类的方法中是非常有用的。在React组件中经常这样做。
参考- https://developer.mozilla.org/en/docs/Web/Javascript/Reference/Global_objects/Function/bind
document.write()用来将一串文本写入由document.open()打开的文档流中。当页面加载后执行document.write()时,它将调用document.open,会清除整个文档(
和会被移除!),并将文档内容替换成给定的字符串参数。因此它通常被认为是危险的并且容易被误用。网上有一些答案,解释了document.write()被用于分析代码中,或者当你想包含只有在启用了 Javascript 的情况下才能工作的样式。它甚至在HTML5样板代码中用于并行加载脚本并保持执行顺序!但是,我怀疑这些使用原因是过时的,现在可以在不使用document.write()的情况下实现。如果我的观点有错,请纠正我。
参考-
https://www.quirksmode.org/blog/archives/2005/06/three_javascrip_1.html
-
https://github.com/h5bp/html5-boilerplate/wiki/script-Loading-Techniques#documentwrite-script-tag
功能检测(feature detection)
功能检测包括确定浏览器是否支持某段代码,以及是否运行不同的代码(取决于它是否执行),以便浏览器始终能够正常运行代码功能,而不会在某些浏览器中出现崩溃和错误。例如:
if ('geolocation' in navigator) { // 可以使用 navigator.geolocation} else { // 处理 navigator.geolocation 功能缺失}
Modernizr是处理功能检测的优秀工具。
功能推断(feature inference)
功能推断与功能检测一样,会对功能可用性进行检查,但是在判断通过后,还会使用其他功能,因为它假设其他功能也可用,例如:
if (document.getElementsByTagName) { element = document.getElementById(id); }
非常不推荐这种方式。功能检测更能保证万无一失。
UA 字符串
这是一个浏览器报告的字符串,它允许网络协议对等方(network protocol peers)识别请求用户代理的应用类型、操作系统、应用供应商和应用版本。它可以通过navigator.userAgent访问。 然而,这个字符串很难解析并且很可能存在欺骗性。例如,Chrome 会同时作为 Chrome 和 Safari 进行报告。因此,要检测 Safari,除了检查 Safari 字符串,还要检查是否存在 Chrome 字符串。不要使用这种方式。
参考-
https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Feature_detection
-
https://stackoverflow.com/questions/20104930/whats-the-difference-between-feature-detection-feature-inference-and-using-th
-
https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent
Ajax(asynchronous Javascript and XML)是使用客户端上的许多Web技术,创建异步Web应用的一种Web开发技术。借助Ajax,Web应用可以异步(在后台)向服务器发送数据和从服务器检索数据,而不会干扰现有页面的显示和行为。通过将数据交换层与表示层分离,Ajax允许网页和扩展Web应用程序动态更改内容,而无需重新加载整个页面。实际上,现在通常将JSON替换为XML,因为 Javascript 对 JSON 有原生支持优势。
XMLHttpRequest API经常用于异步通信。此外还有最近流行的fetch API。
参考-
https://en.wikipedia.org/wiki/Ajax_(programming)
-
https://developer.mozilla.org/en-US/docs/AJAX
优点
-
交互性更好。来自服务器的新内容可以动态更改,无需重新加载整个页面。
-
减少与服务器的连接,因为脚本和样式只需要被请求一次。
-
状态可以维护在一个页面上。Javascript 变量和 DOM 状态将得到保持,因为主容器页面未被重新加载。
-
基本上包括大部分 SPA 的优点。
缺点
-
动态网页很难收藏。
-
如果 Javascript 已在浏览器中被禁用,则不起作用。
-
有些网络爬虫不执行 Javascript,也不会看到 Javascript 加载的内容。
-
基本上包括大部分 SPA 的缺点。
JSONP(带填充的JSON)是一种通常用于绕过Web浏览器中的跨域限制的方法,因为 Ajax 不允许跨域请求。
JSONP 通过


