Javascript面试题

2020/06/16 javascript 面试 共 30173 字,约 87 分钟

下面函数x输出?


function test() {
  var x = 'var', result = [];
  result.push(x);
  try {
    throw 'exception';
  }catch (x) {
    x = 'catch';
  }
  result.push(x);
  return result;
}
// Javascript没有块级作用域的一个例外恰好是其异常处理。
test(); // ['var', 'var']

参考

  • Effective Javascript 第12条:理解变量声明提升

闭包的三个基本事实

  • Javascript允许你引用在当前函数以外定义的变量。
  • 即使外部函数已经返回,当前函数仍然可以引用在外部函数所定义的变量。
  • 闭包可以更新外部变量的值。

参考

  • Effective Javascript 第11条:熟练掌握闭包

请解释事件委托(event delegation)

事件委托是将事件监听器添加到父元素,而不是每个子元素单独设置监听。当触发子元素时,事件会冒泡到父元素,监听器就会触发。 这种技术的好处是:

  • 减少内存的占用,只需要一个父元素的事件处理程序,而不必为每个后代添加事件处理程序
  • 无需从已删除的元素中解绑处理程序,也无需将处理程序绑定到新元素上

参考

[↑] 回到顶部

请简述Javascript中的this

JS中的this是一个相对复杂的概念,不是简单几句能解释清楚的。粗略的讲,函数的调用方式决定了this的值。我阅读了网上很多关于this的文章Arnav Aggrawal写的比较清楚。this取值符合以下规则:

  1. 在调用函数时使用new关键字,函数内的this是一个全新的对象。
  2. 如果applycallbind方法调用、创建一个函数,函数内的this就是作为参数传入这些方法的对象。
  3. 当函数作为对象里的方法被调用时,函数内的this是调用该函数的对象。比如:当obj.method()被调用时,函数内的this将绑定到obj对象
  4. 如果调用函数不符合上述规则,那么this的值指向全局对象(global object)。浏览器环境下指向window对象,但在严格模式下(use strict),this的值为undefined
  5. 如果符合上述多个规则,则较高的规则(1最高,4最低)将决定this值。
  6. 如果该函数是ES2015中的箭头函数,将忽略上面所有规则,this将被设置为它被创建时的上下文。 想获得更深入的解释,请查看他在 Medium 上的文章

参考

[↑] 回到顶部

请解释原型继承(prototypal inheritance)的工作原理

这是一个非常常见的Javascript问题。所有JS对象都有一个__proto__属性,指向它的原型对象。当试图访问一个对象的属性时,如果没有在该对象上找到,它会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或达到原型的末尾(Object.prototype)。这种行为是在模拟经典的继承,但是与其说是继承,不如说是委托(delegation)

参考

[↑] 回到顶部

说说你对AMDCommonJS的了解

存疑
它们都是实现模块体系的方式,直到ES2015出现之前,Javascript一直没有模块体系。CommonJS是同步的,而AMD(Asynchronous Module Definitions)从全称中可以明显看出是异步的。CommonJS的设计是为服务器端开发考虑的,而AMD支持异步加载模块,更适合浏览器。

我发现AMD的语法非常冗长,CommonJS更接近其他语言import声明语句用法习惯。大多数情况下,我认为AMD没有使用的必要,因为如果把所有Javascript都捆绑进一个文件中,将无法得到异步加载的好处。此外,CommonJS语法上更接近Node编写模块的风格,在前后端都使用Jaascript开发之间进行切换时,语境的切换开销较小。

我很高兴看到ES2015的模块加载方案同时支持同步和异步,我们终于可以只使用一种方案了。虽然它尚未在浏览器和Node中完全推出,但是我们可以使用代码转换工具进行转换。

参考

[↑] 回到顶部

请解释下面代码为什么不能用作IIFEfunction foo(){ }();,需要作出哪些修改才能使其成为IIFE

IIFE(Immediately Involved 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

注:function之前的左圆括号是必需的,因为如果不写这个左圆括号,Javascript解释器会试图将关键字function解析为函数声明语句。使用圆括号Javascript解释器才会正确地将其解析为函数定义表达式。使用圆括号是习惯用法,尽管有时候没有必要也不应当省略。《Javascript权威指南 第六版》 8-5节 181 页

参考

[↑] 回到顶部

nullundefined和未声明的变量之间有什么区别?如何检查判断这些状态值?

当你没有提前使用varletconst声明变量,就为一个变量赋值时,该变量是未声明变量(undeclared variables)。未声明变量会脱离当前作用域,成为全局作用域下定义的变量。在严格模式下,给未声明的变量赋值,会抛出ReferenceError错误。和使用全局变量一样,使用未声明变量也是非常不好的做法,应当尽可能避免。要检查判断它们,需要将用到它们的代码放在try/catch语句中。

function foo() {
  x = 1; // 在严格模式下,抛出 ReferenceError 错误
}
foo();
console.log(x); // 1

当一个变量已经声明,但没有赋值时,该变量的值是undefined。如果一个函数的执行结果被赋值给一个变量,但是这个函数却没有返回任何值,那么该变量的值是undefined。要检查它,需要使用严格相等(===);或者使用typeof,它会返回'undefined'字符串。请注意,不能使用非严格相等(==)来检查,因为如果变量值为null,使用非严格相等也会返回true

var foo;
console.log(foo); // undefined
console.log(foo === undefined); // true
console.log(typeof foo === 'undefined'); // true

console.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); // true

console.log(foo == undefined); // true. 错误,不要使用非严格相等!

参考

[↑] 回到顶部

什么是闭包(closure),为什么使用闭包?

闭包是函数和声明该函数的词法环境的组合。词法作用域中使用的域,是变量在代码中声明的位置所决定的。闭包是即使被外部函数返回,依然可以访问到外部(封闭)函数作用域的函数。

注:闭包是指有权访问另一个函数作用域中的变量的函数。《Javascript高级程序设计(第三版)》7-2节 178页 注:有关闭包中的this《Javascript高级程序设计(第三版)》7-2-2节 182页

var name = "The Window"
var object = {
    name: "My Object",
    getNameFunc: function() {
        return function() {
            return this.name
        }
    }
}
console.log(object.getNameFunc()()) // "The Window" (在非严格模式下)
var name = "The Window"
var object = {
    name: "My Object",
    getNameFunc: function() {
        var that = this
        return function() {
            return that.name
        }
    }
}
console.log(object.getNameFunc()()) // "My Object"

为什么使用闭包?

参考

[↑] 回到顶部

请说明.forEach循环和.map()循环的主要区别,它们分别在什么情况下使用?

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

参考

[↑] 回到顶部

匿名函数的典型应用场景是什么?

匿名函数可以在 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]

参考

[↑] 回到顶部

宿主对象(host objects)和原生对象(native objects)的区别是什么?

原生对象是由 ECMAScript 规范定义的 JavaScript 内置对象,比如StringMathRegExpObjectFunction等等。

宿主对象是由运行时环境(浏览器或 Node)提供,比如windowXMLHTTPRequest等等。

参考

[↑] 回到顶部

下列语句有什么区别:function Person(){}var person = Person()var person = new Person()

这个问题问得很含糊。我猜这是在考察 JavaScript 中的构造函数(constructor)。从技术上讲,function Person(){}只是一个普通的函数声明。使用 PascalCase 方式命名函数作为构造函数,是一个惯例。

注:函数声明,首字母大写按惯例当作构造函数使用,会变量提升。

var person = Person()Person以普通函数调用,而不是构造函数。如果该函数是用作构造函数的,那么这种调用方式是一种常见错误。通常情况下,构造函数不会返回任何东西,因此,像普通函数一样调用构造函数,只会返回undefined赋给用作实例的变量。

注:函数定义表达式,首字母大写按惯例当作构造函数使用,不会变量提升。

var person = new Person()使用new操作符,创建Person对象的实例,该实例继承自Person.prototype。另外一种方式是使用Object.create,例如:Object.create(Person.prototype)`。

注:构造函数的使用

参考

[↑] 回到顶部

.call.apply有什么区别?

.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)); // 3
console.log(add.apply(null, [1, 2])); // 3

[↑] 回到顶部

请说明Function.prototype.bind的用法。

摘自MDN

bind()方法创建一个新的函数, 当被调用时,将其 this 关键字设置为提供的值,在调用新函数时,在任何提供之前提供一个给定的参数序列。 注:《Javascript权威指南 第六版》 8-7-4节 190页 《Javascript高级程序设计 第三版》 22-1-4节 602页

function f(y) {return this.x + y;}
var o = {x: 1}
var g = f.bind(o)
g(2) // => 3
var sum = function(x, y) {return x + y;}
var succ = sum.bind(null, 1);
succ(2) // => 3
function f(y,z){return this.x + y + z;}
var g = f.bind({x:1}, 2)
g(3) // => 6

参考

[↑] 回到顶部

什么时候会用到document.write()

document.write()用来将一串文本写入由document.open()打开的文档流中。当页面加载后执行document.write()时,它将调用document.open,会清除整个文档(<head><body>会被移除!),并将文档内容替换成给定的字符串参数。因此它通常被认为是危险的并且容易被误用。

注:当页面加载完成后,执行document.write()会重写整个页面内容,其它还有writeIn()、open()、close()方法,具体参见 《Javascript高级程序设计 第三版》10-1-2-6节 259页

网上有一些答案,解释了document.write()被用于分析代码中,或者当你想包含只有在启用了 JavaScript 的情况下才能工作的样式。它甚至在 HTML5 样板代码中用于并行加载脚本并保持执行顺序!但是,我怀疑这些使用原因是过时的,现在可以在不使用document.write()的情况下实现。如果我的观点有错,请纠正我。

参考

[↑] 回到顶部

功能检测(feature detection)、功能推断(feature inference)和使用 UA 字符串之间有什么区别?

功能检测(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 字符串。不要使用这种方式。

参考

[↑] 回到顶部

请尽可能详细地解释Ajax

Ajax(asynchronous JavaScript and XML)是使用客户端上的许多Web技术,创建异步Web应用的一种Web开发技术。借助AjaxWeb应用可以异步(在后台)向服务器发送数据和从服务器检索数据,而不会干扰现有页面的显示和行为。通过将数据交换层与表示层分离,Ajax允许网页和扩展Web应用程序动态更改内容,而无需重新加载整个页面。实际上,现在通常将XML替换为JSON,因为JavaScriptJSON有原生支持优势。

XMLHttpRequest API 经常用于异步通信。此外还有最近流行的fetch API。

参考

[↑] 回到顶部

使用Ajax的优缺点分别是什么?

优点

  • 交互性更好。来自服务器的新内容可以动态更改,无需重新加载整个页面。
  • 减少与服务器的连接,因为脚本和样式只需要被请求一次。
  • 状态可以维护在一个页面上。JavaScript变量和DOM状态将得到保持,因为主容器页面未被重新加载。
  • 基本上包括大部分SPA的优点。 缺点
  • 动态网页很难收藏。
  • 如果JavaScript已在浏览器中被禁用,则不起作用。
  • 有些网络爬虫不执行JavaScript,也不会看到JavaScript加载的内容。
  • 基本上包括大部分SPA的缺点。

[↑] 回到顶部

请说明JSONP的工作原理,它为什么不是真正的Ajax

JSONP(带填充的 JSON)是一种通常用于绕过Web浏览器中的跨域限制的方法,因为Ajax不允许跨域请求。

JSONP 通过<script>标签发送跨域请求,通常使用callback查询参数,例如:https://example.com?callback=printData。 然后服务器将数据包装在一个名为printData的函数中并将其返回给客户端。

<!-- https://mydomain.com -->
<script>
  function printData(data) {
    console.log(`My name is ${data.name}!`);
  }
</script>

<script src="https://example.com?callback=printData"></script>
// 文件加载自 https://example.com?callback=printData
printData({name: 'Yang Shun'});

客户端必须在其全局范围内具有printData函数,并且在收到来自跨域的响应时,该函数将由客户端执行。

JSONP 可能具有一些安全隐患。由于JSONP是纯JavaScript实现,它可以完成JavaScript所能做的一切,因此需要信任JSONP数据的提供者。

现如今,跨来源资源共享(CORS) 是推荐的主流方式,JSONP 已被视为一种比较 hack 的方式。

参考

[↑] 回到顶部

请解释变量提升(hoisting)。

变量提升(hoisting)是用于解释代码中变量声明行为的术语。使用var关键字声明或初始化的变量,会将声明语句“提升”到当前作用域的顶部。 但是,只有声明才会触发提升,赋值语句(如果有的话)将保持原样。我们用几个例子来解释一下。

// 用 var 声明得到提升
console.log(foo); // undefined
var foo = 1;
console.log(foo); // 1

// 用 let/const 声明不会提升
console.log(bar); // ReferenceError: bar is not defined
let bar = 2;
console.log(bar); // 2

函数声明会使函数体提升,但函数表达式(以声明变量的形式书写)只有变量声明会被提升。

// 函数声明
console.log(foo); // [Function: foo]
foo(); // 'FOOOOO'
function foo() {
  console.log('FOOOOO');
}
console.log(foo); // [Function: foo]

// 函数表达式
console.log(bar); // undefined
bar(); // Uncaught TypeError: bar is not a function
var bar = function () {
  console.log('BARRRR');
};
console.log(bar); // [Function: bar]

[↑] 回到顶部

请描述事件冒泡。

当一个事件在 DOM 元素上触发时,如果有事件监听器,它将尝试处理该事件,然后事件冒泡到其父级元素,并发生同样的事情。最后直到事件到达祖先元素。事件冒泡是实现事件委托的原理(event delegation)。

[↑] 回到顶部

attributeproperty之间有什么区别?

Attribute是在HTML中定义的,而property是在DOM上定义的。为了说明区别,假设我们在 HTML 中有一个文本框:<input type="text" value="Hello">

const input = document.querySelector('input');
console.log(input.getAttribute('value')); // Hello
console.log(input.value); // Hello

但是在文本框中键入 World!后:

console.log(input.getAttribute('value')); // Hello
console.log(input.value); // Hello World!

参考

[↑] 回到顶部

为什么扩展JavaScript内置对象是不好的做法?

扩展JavaScript内置(原生)对象意味着将属性或方法添加到其prototype中。虽然听起来很不错,但事实上这样做很危险。想象一下,你的代码使用了一些库,它们通过添加相同的 contains 方法来扩展Array.prototype,如果这两个方法的行为不相同,那么这些实现将会相互覆盖,你的代码将不能正常运行。

扩展内置对象的唯一使用场景是创建polyfill,本质上为老版本浏览器缺失的方法提供自己的实现,该方法是由 JavaScript 规范定义的。

参考

[↑] 回到顶部

document中的load事件和DOMContentLoaded`事件之间的区别是什么?

当初始的HTML文档被完全加载和解析完成之后,DOMContentLoaded事件被触发,而无需等待样式表、图像和子框架的完成加载。 windowload事件仅在DOM和所有相关资源全部完成加载后才会触发。

参考

[↑] 回到顶部

=====的区别是什么?

==是抽象相等运算符,而===是严格相等运算符。==运算符是在进行必要的类型转换后,再比较。===运算符不会进行类型转换,所以如果两个值不是相同的类型,会直接返回false。使用==时,可能发生一些特别的事情,例如:

1 == '1'; // true
1 == [1]; // true
1 == true; // true
0 == ''; // true
0 == '0'; // true
0 == false; // true

我的建议是从不使用==运算符,除了方便与nullundefined比较时,a == null如果anullundefined将返回true

var a = null;
console.log(a == null); // true
console.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);
}
// or
function duplicate(arr) {
    return [...arr, ...arr];
}
duplicate([1, 2, 3, 4, 5]); // [1,2,3,4,5,1,2,3,4,5]

[↑] 回到顶部

什么是"use strict";?使用它有什么优缺点?

‘use strict’ 是用于对整个脚本或单个函数启用严格模式的语句。严格模式是可选择的一个限制JavaScript的变体一种方式 。 优点:

  • 无法再意外创建全局变量。
  • 会使引起静默失败(silently fail,即:不报错也没有任何效果)的赋值操抛出异常。
  • 试图删除不可删除的属性时会抛出异常(之前这种操作不会产生任何效果)。
  • 要求函数的参数名唯一。
  • 全局作用域下,this的值为undefined
  • 捕获了一些常见的编码错误,并抛出异常。
  • 禁用令人困惑或欠佳的功能。

缺点:

  • 缺失许多开发人员已经习惯的功能。
  • 无法访问function.callerfunction.arguments
  • 以不同严格模式编写的脚本合并后可能导致问题。

参考

[↑] 回到顶部

创建一个循环,从 1 迭代到 100,3的倍数时输出 “fizz”,5的倍数时输出 “buzz”,同时为35的倍数时输出 “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 构建完成后触发,但不要等待其他资源完成加载。如果在初始化之前不需要装入整个页面,这个事件是使用首选。

参考

[↑] 回到顶部

请解释单页应用是什么,如何使其对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,并将其返回给爬虫”。

参考

[↑] 回到顶部

Promise代替回调函数有什么优缺点?

优点:

  • 避免可读性极差的回调地狱。
  • 使用.then()编写的顺序异步代码,既简单又易读。
  • 使用Promise.all()编写并行异步代码变得很容易。

缺点:

  • 轻微地增加了代码的复杂度(这点存在争议)。
  • 在不支持 ES2015 的旧版浏览器中,需要引入 polyfill 才能使用。

[↑] 回到顶部

你使用什么语句遍历对象的属性和数组的元素?

对象:

  • 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++)
  • forEacharr.forEach(function (el, index) { ... })。这个语句结构有时会更精简,因为如果你所需要的只是数组元素,你不必使用index。还有everysome方法可以让你提前终止遍历。

注:还有ES6中的Set集合与Map集合未考虑《深入理解ES6》 美 NICHOLAS C.ZAKAS 著 7章 128页

[↑] 回到顶部

请解释可变对象和不可变对象之间的区别。

  1. JavaScript 中,stringnumber 从设计之初就是不可变(Immutable)。
  2. _不可变_ 其实是保持一个对象状态不变,这样做的好处是使得开发更加简单,可回溯,测试友好,减少了任何可能的副作用。但是,每当你想添加点东西到一个不可变(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 properties
console.log(student1, student2); //Object {school: "Baidu", name: "HOU Ce", birthdate: "1995-12-15"} Object {school: "Baidu", name: "YAN Haijing", birthdate: "1990-11-10"}

参考

[↑] 回到顶部

请解释同步和异步函数之间的区别。

同步函数阻塞,而异步函数不阻塞。在同步函数中,语句完成后,下一句才执行。在这种情况下,程序可以按照语句的顺序进行精确评估,如果其中一个语句需要很长时间,程序的执行会停滞很长时间。

异步函数通常接受回调作为参数,在调用异步函数后立即继续执行下一行。回调函数仅在异步操作完成且调用堆栈为空时调用。诸如从 Web 服务器加载数据或查询数据库等重负载操作应该异步完成,以便主线程可以继续执行其他操作,而不会出现一直阻塞,直到费时操作完成的情况(在浏览器中,界面会卡住)。

[↑] 回到顶部

什么是事件循环?调用堆栈和任务队列之间有什么区别?

事件循环是一个单线程循环,用于监视调用堆栈并检查是否有工作即将在任务队列中完成。如果调用堆栈为空并且任务队列中有回调函数,则将回调函数出队并推送到调用堆栈中执行。

注:图形化应该会更好理解。正好《Learning TypeScript中文版》 5节 136页有更详细的说明。

如果你没有看过 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 function
var foo = function () {
  console.log('FOOOOO');
};

参考

[↑] 回到顶部

使用letvarconst创建变量有什么区别?

var声明的变量的作用域是它当前的执行上下文,它可以是嵌套的函数,也可以是声明在任何函数外的变量。letconst是块级作用域,意味着它们只能在最近的一组花括号(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 defined
console.log(baz); // ReferenceError: baz is not defined
console.log(qux); // ReferenceError: qux is not defined
if (true) {
  var bar = 'bar';
  let baz = 'baz';
  const qux = 'qux';
}

// 用 var 声明的变量在函数作用域上都可访问
console.log(bar); // bar
// let 和 const 定义的变量在它们被定义的语句块之外不可访问
console.log(baz); // ReferenceError: baz is not defined
console.log(qux); // ReferenceError: qux is not defined

var会使变量提升,这意味着变量可以在声明之前使用。letconst不会使变量提升,提前使用会报错。

console.log(foo); // undefined

var foo = 'foo';

console.log(baz); // ReferenceError: can't access lexical declaration 'baz' before initialization

let baz = 'baz';

console.log(bar); // ReferenceError: can't access lexical declaration 'bar' before initialization

const bar = 'bar';

var重复声明不会报错,但letconst会。

var foo = 'foo';
var foo = 'bar';
console.log(foo); // "bar"

let baz = 'baz';
let baz = 'qux'; // Uncaught SyntaxError: Identifier 'baz' has already been declared

letconst的区别在于:let允许多次赋值,而const只允许一次。

// 这样不会报错。
let foo = 'foo';
foo = 'bar';

// 这样会报错。
const baz = 'baz';
baz = 'qux';

参考

[↑] 回到顶部

ES6 的类和 ES5 的构造函数有什么区别?

看一个例子:

// ES5 构造函数
function Person(name) {
  this.name = name;
}

// ES6 类
class Person {
  constructor(name) {
    this.name = name;
  }
}

对于简单的构造函数而言,他们看起来很相似。 他们的主要区别体现在类继承上。如果我们想要创建一个继承于 Person 父类的 Student 子类,并且添加一个 studentId 字段,我们需要做的修改如下:

// ES5 构造函数
function Student(name, studentId) {
  // 调用父类的构造函数来初始化父类的成员变量
  Person.call(this, name);

  // 初始化子类自己的成员变量
  this.studentId = studentId;
}

Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;

// ES6 类
class Student extends Person {
  constructor(name, studentId) {
    super(name);
    this.studentId = studentId;
  }
}

从上面的例子我们可以看出来,使用 ES5 构造函数来实现继承特别麻烦,而使用 ES6 类的方式来实现就特别容易理解和记忆。

参考

[↑] 回到顶部

你能给出一个使用箭头函数的例子吗,箭头函数与其他函数有什么不同?

在构造函数里使用箭头函数的主要优点是它的 this 只与箭头函数创建时的 this 保持一致,并且不会修改。所以,当用构造函数去创建一个新的对象的时候,箭头函数的 this 总是指向新创建的对象。比如,假设我们有一个 Person 构造函数,它接受一个 firstName 参数,并且它有两个方法去调用 console.log 这个 firstName,一个是正常的函数,而另一个则是箭头函数:

const Person = function (firstName) {
  this.firstName = firstName;
  this.sayName1 = function () {
    console.log(this.firstName);
  };
  this.sayName2 = () => {
    console.log(this.firstName);
  };
};

const john = new Person('John');
const dave = new Person('Dave');

john.sayName1(); // John
john.sayName2(); // John

// 普通函数的 this 可以被修改,而箭头函数则不会
john.sayName1.call(dave); // Dave (因为 "this" 现在指向了 dave 对象)
john.sayName2.call(dave); // John

john.sayName1.apply(dave); // Dave (因为 "this" 现在指向了 dave 对象)
john.sayName2.apply(dave); // John

john.sayName1.bind(dave)(); // Dave (因为 "this" 现在指向了 dave 对象)
john.sayName2.bind(dave)(); // John

var sayNameFromWindow1 = john.sayName1;
sayNameFromWindow1(); // undefined (因为 "this" 现在指向了 Window 对象)

var sayNameFromWindow2 = john.sayName2;
sayNameFromWindow2(); // John

这里主要的区别是,正常函数的 this 是可以在执行过程中被改变的,而箭头函数的 this 则会一直保持一致。所以在使用箭头函数的时候,你就不需要担心它的上下文被改变了。

(想看更好的演示以及示例代码,可以打开这篇文章: https://medium.com/@machnicki/handle-events-in-react-with-arrow-functions-ede88184bbb)

[↑] 回到顶部

高阶函数(higher-order)的定义是什么?

高阶函数是将一个或多个函数作为参数的函数,它用于数据处理,也可能将函数作为返回结果。高阶函数是为了抽象一些重复执行的操作。一个典型的例子是map,它将一个数组和一个函数作为参数。map使用这个函数来转换数组中的每个元素,并返回一个包含转换后元素的新数组。JavaScript 中的其他常见示例是forEachfilterreduce。高阶函数不仅需要操作数组的时候会用到,还有许多函数返回新函数的用例。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); // 3
console.log(b); // 1

对象解构

// 变量赋值
const o = {p: 42, q: true};
const {p, q} = o;

console.log(p); // 42
console.log(q); // true

混合解构

let node = {
    type: "Identifier",
    name: "foo",
    loc: {
        start: {
            line: 1,
            column: 1
        },
        end: {
            line: 1,
            column: 4
        }
    },
    range: [0, 3]
}
let {
    loc: { start },
    range: [ startIndex ]
} = node
console.log(start.line) // 1
console.log(start.column) // 1
console.log(startIndex) // 0

解构参数

function setCookie(name, value, options) {
    options = options || {}
    let secure = options.secure,
        path = options.path,
        domain = options.domain,
        expiress = options.expiress;
    // 设置cookie
}
// 解构参数变得更清晰
function setCookie(name, value, { secure, path, domain, expiress }) {
    // 设置cookie
}
setCookie("type", "js", {
    secure: true,
    expiress: 60000
})

参考

[↑] 回到顶部

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 1
string 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 < values.length; index++) {
    output += stringArr[index] + values[index];
  }

  output += stringArr[index];

  return output;
}

message; //"我来给大家介绍:张三的年龄是20."

参考

[↑] 回到顶部

你能举出一个柯里化函数(curry function)的例子吗?它有哪些好处?

柯里化(currying)是一种模式,其中具有多个参数的函数被分解为多个函数,当被串联调用时,将一次一个地累积所有需要的参数。这种技术帮助编写函数式风格的代码,使代码更易读、紧凑。值得注意的是,对于需要被 curry 的函数,它需要从一个函数开始,然后分解成一系列函数,每个函数都需要一个参数。

注:函数柯里化用于创建已经设置好了一个或多个参数的函数。函数柯里化的基本方法和函数绑定是一样的:使用一个闭包返回一个函数。两者的区别在于,当函数被调用时,返回的函数还需要设置一些传入参数。

// 技术上说curriedAdd并非是柯里化函数,但是很好的展示了其概念
function add(num1, num2) {
    return num1 + num2
}
function curriedAdd(num2) {
    return add(5, num2)
}
console.log(add(2, 3))  // 5
console.log(curriedAdd(3)) // 8
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]
function curry(fn) {
    var args = Array.prototype.slice.call(arguments, 1)
    return function() {
        var innerArgs = Array.prototype.slice.call(arguments)
        var finalArgs = args.concat(innerArgs)
        return fn.apply(null, finalArgs)
    }
}
function add(num1, num2) {
    return num1 + num2
}
var curriedAdd = curry(add, 5)
console.log(curriedAdd(3)) // 8

var curriedAdd = curry(add, 5, 12)
console.log(curriedAdd()) // 17

参考

[↑] 回到顶部

使用扩展运算符(spread)的好处是什么,它与使用剩余参数语句(rest)有什么区别?

在函数泛型编码时,ES6 的扩展运算符非常有用,因为我们可以轻松创建数组和对象的拷贝,而无需使用Object.createslice或其他函数库。

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 环境中得到支持。

参考

[↑] 回到顶部

什么情况下会用到静态类成员?

静态类成员(属性或方法)不绑定到某个类的特定实例,不管哪个实例引用它,都具有相同的值。静态属性通常是配置变量,而静态方法通常是纯粹的实用函数,不依赖于实例的状态。

参考

[↑] 回到顶部

打印网页标签个数以及标签最多的一组数据

// tag数
new Set(document.getElementsByTagName("*")).size

// 最多的tag
console.table(Object.entries([...document.getElementsByTagName("*")].map(v=>v.nodeName).reduce((obj, v)=>{
    obj[v] = obj[v]? obj[v]+1: 1;
    return obj
}, {})).sort((a, b)=>b[1]-a[1]).slice(0, 1))

[↑] 回到顶部

函数的防抖和节流

在前端开发过程中,经常会遇到需要绑定一些持续触发的事件,如resizescrollmousemove等等,但有些时候我们并不希望事件持续触发的过程中频繁的去执行函数。一般来讲,防抖和节流是比较好的解决方案。


<!-- 持续触发的情况 -->
<html>
  <body style="margin: 0;">
    <div id="content" style="height:100%;line-height:500px;text-align:center; color: #fff;background-color:#ccc;font-size:80px;"></div>
  </body>
  <script>
    let num = 1;
    let content = document.getElementById('content');
    function count() {
      content.innerHTML = num++;
    };
    content.onmousemove = count;
    // content.onmousemove = debounce(count, 1000);
    // content.onmousemove = throttle(count, 1000);
  </script>
</html>

防抖(debounce) 所谓防抖,就是指触发事件后n秒内只能执行一次,如果在n秒内又触发了事件,则会重新计算函数执行时间。

防抖函数分为非立即执行和立即执行


// 非立即执行
// 函数触发事件后不会立即执行,而是在n秒后执行,如果n秒内又触发了事件,则会重新计cou'tent
function debounce(func, wait) {
  let timeout;
  return function() {
    // let context = this;
    // let args = arguments;
    if(timeout) clearTimeout(timeout);
    timeout = setTimeout(()=>{
      // func.apply(context, args);
      func.apply(this, arguments);
    }, wait);
  };
}

// 立即执行
// 触发事件后函数立即执行,然后n秒内不触发事件才能继续执行函数
function debounce(func, wait) {
  let timeout;
  return function() {
    if(timeout) clearTimeout(timeout);
    let callNow = !timeout;
    timeout = setTimeout(()=>{
      timeout = null;
    }, wait);
    if(callNow) func.apply(this, arguments);
  };
}

/**
 * @desc 函数防抖
 * @param func 函数
 * @param wait 延迟执行毫秒数
 * @param immediate true 表示立即执行,false 表示非立即执行
 */
function debounce(func, wait, immediate) {
  let timeout;
  return function() {
    if(timeout) clearTimeout(timeout);
    if(immediate) {
      let callNow = !timeout;
      timeout = setTimeout(()=>{
        timeout = null;
      }, wait);
      if(callNow) func.applt(this, arguments);
    }else {
      timeout = setTimeout(()=> {
        func.apply(this, arguments);
      }, wait);
    }
  };
}

节流(throttle) 所谓节流,就是指连续触发事件但是在n秒中只执行一次函数。节流会稀释函数执行频率。

对于节流,一般有两种方式可以实现,分别是时间戳版和定时器版。


// 时间戳版
function throttle(func, wait) {
  let previous = 0;
  return function() {
    let now = Date.now();
    if( now - previous > wait ) {
      func.apply(this, arguments);
      previous = now;
    }
  };
}

// 定时器版
function throttle(func, wait) {
  let timeout;
  return function() {
    if(!timeout) {
      timeout = setTimeout(()=>{
        timeout = null;
        func.apply(this, arguments);
      }, wait);
    }
  };
}

// 时间戳版和定时器版节流函数的区别是,时间戳版的函数触发是在时间段开始的时候,而定时器版函数触发是在时间段内结束的时候。

/**
 * @desc 函数节流
 * @param func 函数
 * @param wait 延迟执行毫秒数
 * @param type 1 表时间戳版,2 表定时器版
 */
 function throttle(func, wait, type) {
   if(type===1) {
     let previous = 0;
   }else if(type===2) {
     let timeout;
   }
   return function() {
     if(type===1) {
       let now = Date.now();
       if(now-previous>wait) {
         func.applt(this, arguments);
         previous = now;
       }
     }else if(type===2) {
       if(!timeout) {
         timeout = setTimeout(()=>{
            timeout = null;
            func.apply(this, arguments);
         }, wait);
       }
     }
   }
 }

[↑] 回到顶部

其他

[↑] 回到顶部

写在最后:

  • 原作者yangshun
  • 部分摘录自《JavaScript高级程序设计》(第3版)
  • 部分摘录自《JavaScript权威指南》(第6版)
  • 部分摘录自《深入理解ES6》
  • 部分摘录自《Learning TypeScript中文版》
  • 部分摘录自《Effective Javascript》
  • 感谢他们的付出!

文档信息

Search

    Table of Contents