``原文:70 JavaScript Interview Questions
image.png

70个JS面试问题

1、undefiendnull的区别是什么?

在了解他们之间的差别之前,我们需要熟悉他们之间的相同点。

  • 他们隶属于JS的七大基础类型。

    1. let primitiveTypes = ['string','number','null','undefined','boolean','symbol', 'bigint']
  • 他们是假值。可以转换成布尔值,当用Boolean(value)!!value,返回false。

    1. console.log(!!null); //logs false
    2. console.log(!!undefined); //logs false
    3. console.log(Boolean(null)); //logs false
    4. console.log(Boolean(undefined)); //logs false

    不同点:

  • undefined是变量的默认值 ,当变量没有被指定一个特定的值。或者一个函数没有一个特定的返回值,例如console.log(1)。或者对象里不存在的属性。JS引擎就会分配给一个undefined的值。

    1. let _thisIsUndefined;
    2. const doNothing = () => {};
    3. const someObj = {
    4. a : "ay",
    5. b : "bee",
    6. c : "si"
    7. };
    8. console.log(_thisIsUndefined); //logs undefined
    9. console.log(doNothing()); //logs undefined
    10. console.log(someObj["d"]); //logs undefined
  • null是一个值代表着空值。null是一个值并且被清楚明确的赋给了一个变量。下面的例子里,我们得到了一个null值,当fs.readFile没有抛出错误。

    1. fs.readFile('path/to/file', (e,data) => {
    2. console.log(e); //it logs null when no error occurred
    3. if(e){
    4. console.log(e);
    5. }
    6. console.log(data);
    7. });

    当我们比较undefinednull时,当用半等==的时候,返回的是true;使用全等===返回false。

    1. console.log(null == undefined); // logs true
    2. console.log(null === undefined); // logs false

    2、&&操作符做了什么?

    &&或者逻辑AND操作符会寻找第一个假值表达式,并且返回它;如果没有找到任何的假值表达式,它就返回最后那个表达式。使用短路运算可以避免一些不必要的工作。我在catch语句里使用了这个用来关闭数据库连接。

    1. console.log(false && 1 && []); //logs false
    2. console.log(" " && true && 5); //logs 5

    使用if语句。

    1. const router: Router = Router();
    2. router.get('/endpoint', (req: Request, res: Response) => {
    3. let conMobile: PoolConnection;
    4. try {
    5. //do some db operations
    6. } catch (e) {
    7. if (conMobile) {
    8. conMobile.release();
    9. }
    10. }
    11. });

    使用&&操作符 ```javascript const router: Router = Router();

router.get(‘/endpoint’, (req: Request, res: Response) => { let conMobile: PoolConnection; try { //do some db operations } catch (e) { conMobile && conMobile.release() } });

  1. <a name="DhosE"></a>
  2. ## 3、`|| `操作符做了什么
  3. `||`或者逻辑或运算符会寻找第一个真值表达式 ,并且返回它。这也应用了短路运算避免了一些不必要的工作。<br />在函数中,他也可以用来初始化默认的参数,在ES6默认参数支持之前。
  4. ```javascript
  5. console.log(null || 1 || undefined); //logs 1
  6. function logName(name) {
  7. var n = name || "Mark";
  8. console.log(n);
  9. }
  10. logName(); //logs "Mark"

4、哪一种是转换string到number最快的方式,是使用+号还是一元加法操作符。

根据MDN文档+是转换字符串到数字最快的方式,原因是如果它已经是数字他不会做一些额外的操作。

5、DOM是什么?

DOMDocument Object Model的简写。是Html和XML的API接口。当浏览器一开始解析HTML,document创建一个大的对象,一个基于HTML文档的对象就是DOM。这是一个树状结构的DOM。DOM可用于与特定的元素或者节点交互或者修改。

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <meta http-equiv="X-UA-Compatible" content="ie=edge">
  7. <title>Document Object Model</title>
  8. </head>
  9. <body>
  10. <div>
  11. <p>
  12. <span></span>
  13. </p>
  14. <label></label>
  15. <input>
  16. </div>
  17. </body>
  18. </html>

DOM结构如下所示
image.png

6、什么是事件传播?

当一个DOM元素触发了事件,事件不会仅仅发生在一个元素。在冒泡阶段,事件会向上冒泡,传递到他的父元素,再传递到父父元素,直到一直到达window。在捕获阶段,事件会从window开始向下传播直到到达事件触发的对象。
事件传播有三个阶段:

  • 捕获阶段 - 事件从window开始向下传递直到到达目标元素。

  • 目标阶段 - 事件处于目标元素

  • 冒泡阶段 - 事件从目标元素开始往上冒泡直到到达window

image.png

7、什么是事件冒泡?

但一个DOM元素触发事件,事件会一直往上传递,一直到达window
假使有如下的DOM结构。

  1. <div class="grandparent">
  2. <div class="parent">
  3. <div class="child">1</div>
  4. </div>
  5. </div>

代码如下:

  1. function addEvent(el, event, callback, isCapture = false) {
  2. if (!el || !event || !callback || typeof callback !== 'function') return;
  3. if (typeof el === 'string') {
  4. el = document.querySelector(el);
  5. };
  6. el.addEventListener(event, callback, isCapture);
  7. }
  8. addEvent(document, 'DOMContentLoaded', () => {
  9. const child = document.querySelector('.child');
  10. const parent = document.querySelector('.parent');
  11. const grandparent = document.querySelector('.grandparent');
  12. addEvent(child, 'click', function (e) {
  13. console.log('child');
  14. });
  15. addEvent(parent, 'click', function (e) {
  16. console.log('parent');
  17. });
  18. addEvent(grandparent, 'click', function (e) {
  19. console.log('grandparent');
  20. });
  21. addEvent(document, 'click', function (e) {
  22. console.log('document');
  23. });
  24. addEvent('html', 'click', function (e) {
  25. console.log('html');
  26. })
  27. addEvent(window, 'click', function (e) {
  28. console.log('window');
  29. })
  30. });

addEventListener方法有三个可选的参数useCapture有一个默认值 false,如果是true的话事件发生在冒泡阶段,如果是false的话事件发生在捕获阶段。如果我们点击了child元素,他会打印child,parent,grandparent,html,document和window,这就是事件冒泡。

8、什么是事件捕获?

在事件捕获阶段,事件会一直往下传递,直到触发事件的元素。

9、event.preventDefault()event.stopPropagation()的区别是什么?

event.preventDefault()会阻止元素的默认行为。如果是用在form元素,可以阻止默认提交。如果是用在anchor,他可以阻止默认导航。如果是用在contextmenu,可以阻止显示和消失。event.stopPropagation()可以阻止事件的冒泡,或者阻止事件的发生在冒泡和捕获阶段。

10、怎么知道event.preventDefault()是否用在了一个元素上?

如果我们使用了 event.defaultPrevented属性在事件对象。他会返回一个布尔值,预示着event.preventDefault在特定的元素被调用。

11、为什么obj.someprop.x抛出了错误。

  1. const obj = {};
  2. console.log(obj.someprop.x);

显而易见,抛出错误的原因是我们试图去访问x属性,但同时someprop确实undefined。当对象有不存在的属性时,他会有默认的值就是undefined,而undefined是没有x属性的。

12、什么是event.target?

event.target是事件发生或者事件触发的元素。
示例:比如如下所示,尽管事件绑定在div上,但是打印的button信息。

  1. <div onclick="clickFunc(event)" style="text-align: center;margin:15px;
  2. border:1px solid red;border-radius:3px;">
  3. <div style="margin: 25px; border:1px solid royalblue;border-radius:3px;">
  4. <div style="margin:25px;border:1px solid skyblue;border-radius:3px;">
  5. <button style="margin:10px">
  6. Button
  7. </button>
  8. </div>
  9. </div>
  10. </div>
  1. function clickFunc(event) {
  2. console.log(event.target);
  3. }

13、什么是event.currentTarget?

event.currentTarget是绑定事件的元素。

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

半等==和全等 ===的区别是,半等==会在强制转换后比较值,而全等 ===会比较值和类型,在不强制转换的情况下。

强制转换是把值转换成另一种类型。==会有强制转换的过程。==有很多操作要做在比较两个值之前。
假使我们在比较x == y的值。

1、假使x和y有相同的类型。那么就用=== 操作符。
2、如果xnullyundefined,就返回true,反之亦然。
3、如果x是类型number,y是类型string,就会返回x == toNumber(y), 如果x是string,y是number,那么就返回toNumber(x) == y
4、如果x是boolean,那么返回toNumber(x) == y;如果y是boolean,就会返回toNumber(y) == x
5、如果x是stringsymbol或者number,y是object,就会返回x == toPrimitive(y), 如果x是object,或者stringsymbol,就会返回y == toPrimitive(x)
6、返回 false

注意:toPrimitive首先使用对象中的valueOf方法,然后是toString方法来获取该对象的原始值。

x y x == y
5 5 true
1 ‘1’ true
null undefined true
0 false true
‘1, 2’ [1, 2] true
‘[object, object]’ {} true
x y x === y
5 5 true
1 ‘1’ false
null undefined false
0 false false
‘1, 2’ [1, 2] false
‘[object, object]’ {} false

15、当比较两个相同的对象 为什么返回false。

  1. let a = { a: 1 };
  2. let b = { a: 1 };
  3. let c = a;
  4. console.log(a === b); // logs false even though they have the same property
  5. console.log(a === c); // logs true hmm

JS比较对象和基本类型是不同的。对于基本类型,比较是值的大小,而对于对象比较的是引用或者变量所存储的内存地址。

16、!!操作符做了什么?

双重否定和!!会强制把右边的值转换成布尔值。这是一种非正式方式转换成布尔值。

  1. console.log(!!null); //logs false
  2. console.log(!!undefined); //logs false
  3. console.log(!!''); //logs false
  4. console.log(!!0); //logs false
  5. console.log(!!NaN); //logs false
  6. console.log(!!' '); //logs true
  7. console.log(!!{}); //logs true
  8. console.log(!![]); //logs true
  9. console.log(!!1); //logs true
  10. console.log(!![].length); //logs false

17、如何在一行里计算多个表达式?

我们可以使用,或者逗号操作符来计算多表达式在你一行里。它会从左到右计算,会返回最右边的那一项的值或者最后一个运算对象。
如下所示,从左到右计算: x = 27

  1. let x = 5;
  2. x = (x++ , x = addFive(x), x *= 2, x -= 5, x += 10);
  3. function addFive(num) {
  4. return num + 5;
  5. }

18、什么是提升?

提升是一个术语用来描述定义的变量或者函数被移动到了作用域的顶层。
首先要了解执行上下文,执行上下文是代码的执行环境。执行上下文有两个阶段编译和执行。

编译 - 在这个阶段,会获取所有的函数声明,并且把他们提升到作用域的顶层,以便于之后引用它们;获取所有的变量声明,并且把他们提升到作用域的顶层。

执行 - 在这个阶段会把值分配给之前声明的变量和执行或者调用函数。

假使我们有如下代码,在全局作用域:

  1. console.log(y);
  2. y = 1;
  3. console.log(y);
  4. console.log(greet("Mark"));
  5. function greet(name){
  6. return 'Hello ' + name + '!';
  7. }
  8. var y;

上述会分别打印undefined,1 , Hello Mark;
编译阶段会看起来想这样:

  1. function greet(name) {
  2. return 'Hello ' + name + '!';
  3. }
  4. var y; //implicit "undefined" assignment
  5. //waiting for "compilation" phase to finish
  6. //then start "execution" phase
  7. /*
  8. console.log(y);
  9. y = 1;
  10. console.log(y);
  11. console.log(greet("Mark"));
  12. */

编译阶段结束后,执行阶段开始,开始调用函数和给变量分配值。

  1. function greet(name) {
  2. return 'Hello ' + name + '!';
  3. }
  4. var y;
  5. //start "execution" phase
  6. console.log(y);
  7. y = 1;
  8. console.log(y);
  9. console.log(greet("Mark"));

19、什么是作用域?

作用域是指在JS中 一块区域 我们能够合法的访问变量和函数。
JS有三种类型的作用域,全局作用域,函数作用域和块级作用域(ES6)。

  • 全局作用域 - 函数变量在全局作用域声明,可以在任何地方被访问到。

    1. //global namespace
    2. var g = "global";
    3. function globalFunc(){
    4. function innerFunc(){
    5. console.log(g); // can access "g" because "g" is a global variable
    6. }
    7. innerFunc();
    8. }
  • 函数作用域 - 函数、变量和参数在函数内部声明,只能在函数内部被访问,外部不能访问。

    1. function myFavoriteFunc(a) {
    2. if (true) {
    3. var b = "Hello " + a;
    4. }
    5. return b;
    6. }
    7. myFavoriteFunc("World");
    8. console.log(a); // Throws a ReferenceError "a" is not defined
    9. console.log(b); // does not continue here
  • 块级作用域 - 变量(let, const)在块内**_{}_**被声明,只能在块内被访问。

    1. function testBlock(){
    2. if(true){
    3. let z = 5;
    4. }
    5. return z;
    6. }
    7. testBlock(); // Throws a ReferenceError "z" is not defined

    作用域是一套寻找变量的规则。如果变量不存在于当前的作用域,它会检查和查找外部作用域,如果还不存在,就会在一直查找,直到查找到全局作用域,如果仍然没有找到,就会报错。它会查找最近的变量,一旦找到就会停止搜索和查找。这就是作用域链。

    1. /* Scope Chain
    2. Inside inner function perspective
    3. inner's scope -> outer's scope -> global's scope
    4. */
    5. //Global Scope
    6. var variable1 = "Comrades";
    7. var variable2 = "Sayonara";
    8. function outer(){
    9. //outer's scope
    10. var variable1 = "World";
    11. function inner(){
    12. //inner's scope
    13. var variable2 = "Hello";
    14. console.log(variable2 + " " + variable1);
    15. }
    16. inner();
    17. }
    18. outer();
    19. // logs Hello World
    20. // because (variable2 = "Hello") and (variable1 = "World") are the nearest
    21. // variables inside inner's scope.

    image.png

    20、什么是闭包

    闭包是函数的一种能力,能够记住 当前作用域的、父作用域的、父父作用域的、甚至在全局作用域的变量和参数的引用在作用域链的帮助下。

    1. //Global's Scope
    2. var globalVar = "abc";
    3. function a(){
    4. //testClosures's Scope
    5. console.log(globalVar);
    6. }
    7. a(); //logs "abc"
    8. /* Scope Chain
    9. Inside a function perspective
    10. a's scope -> global's scope
    11. */

    在这个例子里,当生命了函数a,全局作用域就是 a 的闭包。
    image.png
    一个更复杂的例子: ```javascript var globalVar = “global”; var outerVar = “outer”

function outerFunc(outerParam) { function innerFunc(innerParam) { console.log(globalVar, outerParam, innerParam); } return innerFunc; }

const x = outerFunc(outerVar); outerVar = “outer-2”; globalVar = “guess” x(“inner”);

  1. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/231258/1643863314798-f8a536e6-a220-499c-8a46-079614f5bf9b.png#clientId=ucd288c1c-60fd-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=411&id=u42676a7d&margin=%5Bobject%20Object%5D&name=image.png&originHeight=382&originWidth=342&originalType=binary&ratio=1&rotation=0&showTitle=false&size=23444&status=done&style=none&taskId=u5672ea09-5511-4e46-a825-f2fa6b2b044&title=&width=368)<br />这会打印 `’guess outer inner‘`,当我们调用`outerFunc`函数的时候,把函数的返回值也就是`innerFunc`赋值给变量`x`,`outerParam`的值是 outer,尽管我们给它分配给了新的值outer-2;<br />原因是重新赋值操作是发生在`outerFunc`函数执行之后,当调用`outerFunc`函数的时候,它会查找`outerVar`的值通过作用域链。当我们调用变量`x`的时候,变量`x`是 `innerFunc`的引用,`innerParam`的值是inner,这是因为在调用`x`的时候,`globalVar`被赋予了一个新的值guess,所以最后的打印是`’guess outer inner‘`。
  2. 看下面的例子:
  3. ```javascript
  4. const arrFuncs = [];
  5. for(var i = 0; i < 5; i++){
  6. arrFuncs.push(function (){
  7. return i;
  8. });
  9. }
  10. console.log(i); // i is 5
  11. for (let i = 0; i < arrFuncs.length; i++) {
  12. console.log(arrFuncs[i]()); // all logs "5"
  13. }

由于闭包的存在并不想预想的那样打印结果。
关键字var定义了一个全局的变量,当我们push了一个函数的时候,返回了一个全局变量i。当我们在循环中调用这些函数的时候,打印结果都是5,这是因为当前i5,此时i是全局变量并且值是5。这是因为闭包保持了变量的引用而不是值在它创建的时候。我们可以通过IIFES(立即执行表达式来解决)或者把var改成let

21、JS中的假值是什么?

  1. const falsyValues = ['', 0, null, undefined, NaN, false];

所谓JS中的假值 是指通过布尔转换能转成false的值。

22、如果检查一个值是不是假值?

可以使用Boolean函数或者双重否定操作符!!

23、’’use strict” 做了什么?

"use strict "是ES5的特性,可以使我们的代码进入严格模式,可以作用于函数和整个脚本文件。严格模式可以提前避免一些bug通过一些限制。

这些限制如下所示:

  • 分配或者访问没有声明的变量

    1. function returnY(){
    2. "use strict";
    3. y = 123;
    4. return y;
    5. }
  • 给只读或者只写的全局变量赋值。

    1. "use strict";
    2. var NaN = NaN;
    3. var undefined = undefined;
    4. var Infinity = "and beyond";
  • 删除不可删除的属性。

    1. "use strict";
    2. const obj = {};
    3. Object.defineProperty(obj, 'x', {
    4. value : '1'
    5. });
    6. delete obj.x;
  • 重复的参数名称。

    1. "use strict";
    2. function someFunc(a, b, b, c){
    3. }
  • eval函数来声明变量。 ```javascript “use strict”;

    eval(“var x = 1;”);

    console.log(x); //Throws a Reference Error x is not defined

  1. - `this`的默认值将会是`undefined`
  2. ```javascript
  3. "use strict";
  4. function showMeThis(){
  5. return this;
  6. }
  7. showMeThis(); //returns undefined

还有更多的限制在严格模式下。

24、JS中this的值是什么?

this的值是当前执行或者调用的函数中对象的值。这里当前的原因是因为this值的变化依赖于当前的上下文环境。

  1. const carDetails = {
  2. name: "Ford Mustang",
  3. yearBought: 2005,
  4. getName(){
  5. return this.name;
  6. },
  7. isRegistered: true
  8. };
  9. console.log(carDetails.getName()); // logs Ford Mustang

上面的示例中,打印了Ford Mustang这跟我们预想中是一致的,这是因为this在这个上下文中指的是carDetails
再添加几行代码,让他看起来比较不一样。

  1. var name = "Ford Ranger";
  2. var getCarName = carDetails.getName;
  3. console.log(getCarName()); // logs Ford Ranger

在第二个console.log打印了Ford Ranger,这看起来 和之前打印的不一致。原因是getCarName方法现在有一了一个不一样的对象 这就是window对象。用var来声明变量,变量会暴露在全局作用域下 并且作为window的属性。this在全局作用域下指的是window对象但前提是’use strict‘没有使用的情况下。

  1. console.log(getCarName === window.getCarName); //logs true
  2. console.log(getCarName === this.getCarName); // logs true

thiswindow指的是同一个对象在这个例子里。
一种解决这个问题的方式就是使用 applycall

  1. console.log(getCarName.apply(carDetails)); //logs Ford Mustang
  2. console.log(getCarName.call(carDetails)); //logs Ford Mustang

applycall的第一个参数是一个对象,而这个对象就是函数里this的值。

IIFE 立即执行函数,在全局作用域下定义的函数,匿名函数,还有对象内部定义的内部函数 中的**this**都指向**window**

  1. (function (){
  2. console.log(this);
  3. })(); //logs the "window" object
  4. function iHateThis(){
  5. console.log(this);
  6. }
  7. iHateThis(); //logs the "window" object
  8. const myFavoriteObj = {
  9. guessThis(){
  10. function getThis(){
  11. console.log(this);
  12. }
  13. getThis();
  14. },
  15. name: 'Marko Polo',
  16. thisIsAnnoying(callback){
  17. callback();
  18. }
  19. };
  20. myFavoriteObj.guessThis(); //logs the "window" object
  21. myFavoriteObj.thisIsAnnoying(function (){
  22. console.log(this); //logs the "window" object
  23. });

如果你想获取myFavoriteObj对象内部的name属性 ’Mark Polo‘,有两种方式可以解决这个问题。

第一,用一个变量来保存this的值。

  1. const myFavoriteObj = {
  2. guessThis(){
  3. const self = this; //saves the this value to the "self" variable
  4. function getName(){
  5. console.log(self.name);
  6. }
  7. getName();
  8. },
  9. name: 'Marko Polo',
  10. thisIsAnnoying(callback){
  11. callback();
  12. }
  13. };

在这个函数中,我们保存了myFavoriteObj对象的this值。

第二,用ES6 的箭头函数。

  1. const myFavoriteObj = {
  2. guessThis(){
  3. const getName = () => {
  4. //copies the value of "this" outside of this arrow function
  5. console.log(this.name);
  6. }
  7. getName();
  8. },
  9. name: 'Marko Polo',
  10. thisIsAnnoying(callback){
  11. callback();
  12. }
  13. };

箭头函数没有自己的this。在上面这个例子里,箭头函数内部的this拷贝了外部函数的值,也就是myFavoriteObj对象。

25、什么是object的 prototype

prototype是最简单的概念,是一个对象的蓝图。是一个备用当属性和方法在当前对象中不存在的时候。这是一种在对象之间共享属性和方法的方式。这也是JS原型继承的核心概念。

  1. const o = {};
  2. console.log(o.toString()); // logs [object Object]

即使o.toString()方法不存在于o对象上,但是并没有报错并且返回字符[object Object] ,当一个属性不存在于对象上,就会检查他的原型,如果仍然不存在,就会检查原型的原型,直到直到该属性,这个就是原型链。原型链的终点就是Object.prototype

  1. console.log(o.toString === Object.prototype.toString); // logs true
  2. // which means we we're looking up the Prototype Chain and it reached
  3. // the Object.prototype and used the "toString" method.

26、什么是IIFE立即执行函数,怎么使用?

IIFE(Immediately Invoked Function Expression) 理解执行函数是一个函数,是在声明之后立即调用或者执行的函数。语法上一般是用()来包裹function(){},之后用另一个()来调用该函数,一般看起来是这样的(function(){}())

  1. (function () {
  2. }());
  3. (function () {
  4. })();
  5. (function named(params) {
  6. })();
  7. (() => {
  8. })();
  9. (function (global) {
  10. })(window);
  11. const utility = (function () {
  12. return {
  13. //utilities
  14. };
  15. })();

上面的示例都是合法的IIFE。
IIFE函数的最佳用途是在函数初始化的时候,避免名称冲突的问题,和其他的变量在全局作用域下,或者污染全局命名空间。如下示例:

  1. <script src="https://cdnurl.com/somelibrary.js"></script>

假使我们有一个外部链接,有一个JS库,会暴露一些全局函数,假使有两个函数叫createGraphdrawGraph,但是这两个函数有bug,我们行创建自己的函数createGraphdrawGraph

  • 一种方式解决这个问题就是改变脚本的结构。

    1. <script src="https://cdnurl.com/somelibrary.js"></script>
    2. <script>
    3. function createGraph() {
    4. // createGraph logic here
    5. }
    6. function drawGraph() {
    7. // drawGraph logic here
    8. }
    9. </script>

    我们用这种方式来覆盖JS库中暴露的方法。

  • 另一种解决这种问题的方法就是修改方法的名称。

    1. <script src="https://cdnurl.com/somelibrary.js"></script>
    2. <script>
    3. function myCreateGraph() {
    4. // createGraph logic here
    5. }
    6. function myDrawGraph() {
    7. // drawGraph logic here
    8. }
    9. </script>

    当我们使用这种解决方案的时候,需要修改调用函数的函数名称。

  • 另一种方式是用IIFE

    1. <script src="https://cdnurl.com/somelibrary.js"></script>
    2. <script>
    3. const graphUtility = (function () {
    4. function createGraph() {
    5. // createGraph logic here
    6. }
    7. function drawGraph() {
    8. // drawGraph logic here
    9. }
    10. return {
    11. createGraph,
    12. drawGraph
    13. }
    14. })();
    15. </script>

    在这个解决方案中,我们定义了一个变量,就是IIFE的结果,返回一个对象包含了两个函数createGraphdrawGraph
    另一个IIFE解决的问题就是下面:

  1. var li = document.querySelectorAll('.list-group > li');
  2. for (var i = 0, len = li.length; i < len; i++) {
  3. li[i].addEventListener('click', function (e) {
  4. console.log(i);
  5. })
  6. }

假使我们有一个ul元素,有一个list-group类名,我们想打印每一个i,当我们每次点击一个单独的li元素的时候。
但是在这段代码中并没有起作用,每一次点击li都打印了5。这里问题的原因就是由于闭包的作用。
闭包是不过是函数的功能能记住变量的引用,当前作用域下的,父作用域下的,或者全局作用域下的。
当我们用var关键字在全局作用域下声明变量,显而易见我们创建了一个全局变量i。所以当我们点击li元素的时候打印5,这是因为我们是在回调函数里调用i,引用的值是5。

  • 一个解决方案就是IIFE。 ```javascript var li = document.querySelectorAll(‘.list-group > li’); for (var i = 0, len = li.length; i < len; i++) { (function (currentIndex) {
    1. li[currentIndex].addEventListener('click', function (e) {
    2. console.log(currentIndex);
    3. })
    })(i); }
  1. 这个解决方案管用的原因是IIFE创建了一个新的作用域为每一个迭代,我们捕获了`i`的值,并把它当成参数`currentIndex`传递给了函数,那么在IIFE调用时每一个`currentIndex`都是不同的。
  2. <a name="OMgB1"></a>
  3. ## 27、`Function.prototype.apply`方法是什么?
  4. `apply`调用函数的时候会指定`this`,或在调用的时候指定函数的拥有对象。
  5. ```javascript
  6. const details = {
  7. message: 'Hello World!'
  8. };
  9. function getMessage(){
  10. return this.message;
  11. }
  12. getMessage.apply(details); // returns 'Hello World!'

这个方法类似call,不同的是传递的参数不同。apply传递的参数是数组。

  1. const person = {
  2. name: "Marko Polo"
  3. };
  4. function greeting(greetingMessage) {
  5. return `${greetingMessage} ${this.name}`;
  6. }
  7. greeting.apply(person, ['Hello']); // returns "Hello Marko Polo!"

28、Function.prototype.call方法是什么?

call调用一个函数指定this或者调用时函数的拥有对象。

  1. const details = {
  2. message: 'Hello World!'
  3. };
  4. function getMessage(){
  5. return this.message;
  6. }
  7. getMessage.call(details); // returns 'Hello World!'

apply函数类型,不同是传递的参数不同。call传递每一个参数,并用逗号,作为区分。

  1. const person = {
  2. name: "Marko Polo"
  3. };
  4. function greeting(greetingMessage) {
  5. return `${greetingMessage} ${this.name}`;
  6. }
  7. greeting.call(person, 'Hello'); // returns "Hello Marko Polo!"

29、Function.prototype.apply 和Function.prototype.call区别是什么?

applycall的区别是传递参数的不同。apply传递的是参数数组,call传递的是一个个的参数。

  1. const obj1 = {
  2. result:0
  3. };
  4. const obj2 = {
  5. result:0
  6. };
  7. function reduceAdd(){
  8. let result = 0;
  9. for(let i = 0, len = arguments.length; i < len; i++){
  10. result += arguments[i];
  11. }
  12. this.result = result;
  13. }
  14. reduceAdd.apply(obj1, [1, 2, 3, 4, 5]); // returns 15
  15. reduceAdd.call(obj2, 1, 2, 3, 4, 5); // returns 15

30、Function.prototype.bind函数的用法;

bind方法会返回一个新的函数,并且绑定到特定的this值或者拥有对象,我们可以后面用它在代码里。callapply方法会立即调用,而bind函数会返回一个新的函数。

  1. import React from 'react';
  2. class MyComponent extends React.Component {
  3. constructor(props){
  4. super(props);
  5. this.state = {
  6. value : ""
  7. }
  8. this.handleChange = this.handleChange.bind(this);
  9. // Binds the "handleChange" method to the "MyComponent" component
  10. }
  11. handleChange(e){
  12. //do something amazing here
  13. }
  14. render(){
  15. return (
  16. <>
  17. <input type={this.props.type}
  18. value={this.state.value}
  19. onChange={this.handleChange}
  20. />
  21. </>
  22. )
  23. }
  24. }

31、什么是函数式编程和JS的那些特性使他成为一种函数式编程语言。

函数式编程是声明式语言编程范式吗,或者是一种模式用函数来创建应用计算值不需要修改或者更高传递给它的参数。
JS数组有map、filter、reduce的方法是最出名的方法在函数式编程世界里由于他们的有效性,是因为他们不会更改数组,这会使我们的函数比较纯洁;JS支持闭包和高阶函数这也是函数式编程的特色。

  • map函数会创建一个新的数组,并针对每一个元素,返回回调函数的结果。 ```javascript const words = [“Functional”, “Procedural”, “Object-Oriented”];

const wordsLength = words.map(word => word.length);

  1. - `filter`函数会返回新的数组,在所有的元素通过回调函数的测试。
  2. ```javascript
  3. const data = [
  4. { name: 'Mark', isRegistered: true },
  5. { name: 'Mary', isRegistered: false },
  6. { name: 'Mae', isRegistered: true }
  7. ];
  8. const registeredUsers = data.filter(user => user.isRegistered);
  • reduce函数对元素执行累加操作,从左到右,最后累加到一个单独唯一的值。 ```javascript const strs = [“I”, “ “, “am”, “ “, “Iron”, “ “, “Man”]; const result = strs.reduce((acc, currentStr) => acc + currentStr, “”);
  1. <a name="w76kB"></a>
  2. ## 32、什么是高阶函数?
  3. 高阶函数是一个函数 ,返回一个函数 或者参数有一个函数值。
  4. ```javascript
  5. function higherOrderFunction(param,callback){
  6. return callback(param);
  7. }

33、为什么函数是第一类对象?

函数是第一类对象在JS中,是因为他们可以被当成其他的值。可以被分配给变量,或者当成对象的属性叫做方法,可作为数组的项,或者作为参数传递给函数,也可作为函数的值返回。唯一的区别函数和其他值,就是函数可以被调用或者执行。

34、手动实现Array.prototype.map方法。

  1. function map(arr, mapCallback) {
  2. // First, we check if the parameters passed are right.
  3. if (!Array.isArray(arr) || !arr.length || typeof mapCallback !== 'function') {
  4. return [];
  5. } else {
  6. let result = [];
  7. // We're making a results array every time we call this function
  8. // because we don't want to mutate the original array.
  9. for (let i = 0, len = arr.length; i < len; i++) {
  10. result.push(mapCallback(arr[i], i, arr));
  11. // push the result of the mapCallback in the 'result' array
  12. }
  13. return result; // return the result array
  14. }
  15. }

就像MDN描述的那样,map方法返回一个新的数组,对每一个数组元素通过执行提供的函数返回新的值。

35、手动实现Array.prototype.filter

  1. function filter(arr, filterCallback) {
  2. // First, we check if the parameters passed are right.
  3. if (!Array.isArray(arr) || !arr.length || typeof filterCallback !== 'function')
  4. {
  5. return [];
  6. } else {
  7. let result = [];
  8. // We're making a results array every time we call this function
  9. // because we don't want to mutate the original array.
  10. for (let i = 0, len = arr.length; i < len; i++) {
  11. // check if the return value of the filterCallback is true or "truthy"
  12. if (filterCallback(arr[i], i, arr)) {
  13. // push the current item in the 'result' array if the condition is true
  14. result.push(arr[i]);
  15. }
  16. }
  17. return result; // return the result array
  18. }
  19. }

filter()会返回一个新的数组,通过对每一个数组元素通过检测,检测是通过提供的函数实现。

36、手动实现Array.prototype.reduce

  1. function reduce(arr, reduceCallback, initialValue) {
  2. // First, we check if the parameters passed are right.
  3. if (!Array.isArray(arr) || !arr.length || typeof reduceCallback !== 'function')
  4. {
  5. return [];
  6. } else {
  7. // If no initialValue has been passed to the function we're gonna use the
  8. let hasInitialValue = initialValue !== undefined;
  9. let value = hasInitialValue ? initialValue : arr[0];
  10. // first array item as the initialValue
  11. // Then we're gonna start looping at index 1 if there is no
  12. // initialValue has been passed to the function else we start at 0 if
  13. // there is an initialValue.
  14. for (let i = hasInitialValue ? 0 : 1, len = arr.length; i < len; i++) {
  15. // Then for every iteration we assign the result of the
  16. // reduceCallback to the variable value.
  17. value = reduceCallback(value, arr[i], i, arr);
  18. }
  19. return value;
  20. }
  21. }

reduce()通过执行一个累加函数(用户提供的)针对数组里每一个元素,最终返回一个单一的值。

37、什么是参数对象?

arguments对象是一组传递给函数的参数值。这是一个类数组对象,因为他有length属性,他可以通过数组的索引符号访问值arguments[1],但是他没有内置方法例如:forEachreducefiltermap
这会帮助我们知道传递给函数的参数个数。
可以用Array.prototype.slicearguments对象转成数组。

  1. function one() {
  2. return Array.prototype.slice.call(arguments);
  3. }

arguments对象不会工作在ES6的箭头函数。

  1. function one() {
  2. return arguments;
  3. }
  4. const two = function () {
  5. return arguments;
  6. }
  7. const three = function three() {
  8. return arguments;
  9. }
  10. const four = () => arguments;
  11. four(); // Throws an error - arguments is not defined

当调用four()函数的时候,会抛出异常ReferenceError: arguments is not defined。如果你的环境支持rest语法,就可以解决这个问题。

  1. const four = (...args) => args;

可以把自动的把所有的参数值放在一个数组里。

38、如何不通过prototype创建一个对象。

不通过prototype创建一个对象 ,可以使用Object.create()

  1. const o1 = {};
  2. console.log(o1.toString());
  3. // logs [object Object] get this method to the Object.prototype
  4. const o2 = Object.create(null);
  5. // the first parameter is the prototype of the object "o2" which in this
  6. // case will be null specifying we don't want any prototype
  7. console.log(o2.toString());
  8. // throws an error o2.toString is not a function

39、当你调用这个函数的时候,为什么b会变成全局变量?

  1. function myFunc() {
  2. let a = b = 0;
  3. }
  4. myFunc();

原因是分配操作符或者=有从右到左的关联性或者评估。当多个分配操作符出现在单一的表达式里,他们会从右到左计算。代码如下:

  1. function myFunc() {
  2. let a = (b = 0);
  3. }
  4. myFunc();

首先,表达式b=0被计算,在这个例子里b没有被声明。JS引擎把b做成全局变量,在返回表达式b=0计算的值 0,并且把它分配给新的本地变量a,用let关键字。

可以通过先声明变量在分配值之前。

  1. function myFunc() {
  2. let a,b;
  3. a = b = 0;
  4. }
  5. myFunc();

40、ECMAScript是什么?

ECMAScript是一种标准,JS会遵循在ECMAScript标准里规范的变化,因为这是JS的规划蓝图。

41、ES6或者ECMAScript 2005的新特性?

1、箭头函数
2、类
3、模板字符串
4、增强的对象字面量
5、对象解构
6、Promise
7、Generator
8、模块
9、Symbol
10、代理
11、Sets
12、默认的函数参数
13、Rest和扩展运算符
14、块级作用域,let和const

42、关键字 var、let和const的区别?

var声明的变量是函数作用域。
这意味着var声明的变量可被跨函数作用域访问,即时声明的函数在块级区域内。

  1. function giveMeX(showX) {
  2. if (showX) {
  3. var x = 5;
  4. }
  5. return x;
  6. }
  7. console.log(giveMeX(false));
  8. console.log(giveMeX(true));

第一个打印出来undefined,第二个是5
我们可以访问到x变量,由于x提升到函数作用域的顶端。如下所示:

  1. function giveMeX(showX) {
  2. var x; // has a default value of undefined
  3. if (showX) {
  4. x = 5;
  5. }
  6. return x;
  7. }

第一个打印出undefined的原因是当声明一个变量没有初始值 就会给一个默认值undefined

letvar声明的变量是块级作用域的。这意味着变量只能在变量声明的块{}内访问。

  1. function giveMeX(showX) {
  2. if (showX) {
  3. let x = 5;
  4. }
  5. return x;
  6. }
  7. function giveMeY(showY) {
  8. if (showY) {
  9. let y = 5;
  10. }
  11. return y;
  12. }

当参数是false的时候,调用函数会抛出 Reference Error,我们不能访问xy在块级作用域之外,这些变量并没有被提升。

letconst的区别是:我们可以分配一个新的值给let声明的变量,但是却不能分配一个新的值给const。这里的意思是,如果我们分配一个对象给一个const变量,我们可以改变该对象里面的属性值,但是不能重新分配新的值给这个const变量。

43、箭头函数是什么?

箭头函数是JS中定义函数的新方式。箭头函数可以花费很少的时间创建函数,并且这是一种更简洁的方式相比于函数表达式,因为这省略了 function关键字。

  1. //ES5 Version
  2. var getCurrentDate = function (){
  3. return new Date();
  4. }
  5. //ES6 Version
  6. const getCurrentDate = () => new Date();

在这个例子里,ES5版本有函数function(){}表达式和return关键字来创建一个函数,并且需要单独返回值。在箭头函数版本中,我们只需要括号(),不需要return语句。因为箭头函数有一个确切的return如果它只有一个表达式或者值返回。

  1. //ES5 Version
  2. function greet(name) {
  3. return 'Hello ' + name + '!';
  4. }
  5. //ES6 Version
  6. const greet = (name) => `Hello ${name}`;
  7. const greet2 = name => `Hello ${name}`;

箭头函数同样可以设置参数就像函数表达式和函数声明语句一样。如果我们有一个参数,我们可以省略括号()

  1. const getArgs = () => arguments
  2. const getArgs2 = (...rest) => rest

箭头函数不能访问arguments对象。所以调用getArgs函数会抛出错误。作为替换,我们可以用rest参数来获取所有的参数传递给箭头函数的。

  1. const data = {
  2. result: 0,
  3. nums: [1, 2, 3, 4, 5],
  4. computeResult() {
  5. // "this" here refers to the "data" object
  6. const addAll = () => {
  7. // arrow functions "copies" the "this" value of
  8. // the lexical enclosing function
  9. return this.nums.reduce((total, cur) => total + cur, 0)
  10. };
  11. this.result = addAll();
  12. }
  13. };

箭头函数没有他自己的this值。他会捕获或者获取词法封闭函数的this值,在这个例子中,addAll会拷贝computeResult方法的this值,如果我们在全局作用域声明箭头函数,那么this值就指向window

44、类是什么

类是写构造函数的新方式。这是构造函数的语法糖,本质上还是原型和基于原型的继承。

  1. //ES5 Version
  2. function Person(firstName, lastName, age, address){
  3. this.firstName = firstName;
  4. this.lastName = lastName;
  5. this.age = age;
  6. this.address = address;
  7. }
  8. Person.self = function(){
  9. return this;
  10. }
  11. Person.prototype.toString = function(){
  12. return "[object Person]";
  13. }
  14. Person.prototype.getFullName = function (){
  15. return this.firstName + " " + this.lastName;
  16. }
  17. //ES6 Version
  18. class Person {
  19. constructor(firstName, lastName, age, address){
  20. this.lastName = lastName;
  21. this.firstName = firstName;
  22. this.age = age;
  23. this.address = address;
  24. }
  25. static self() {
  26. return this;
  27. }
  28. toString(){
  29. return "[object Person]";
  30. }
  31. getFullName(){
  32. return `${this.firstName} ${this.lastName}`;
  33. }
  34. }

重载方法或者从其他类中继承方法。

  1. //ES5 Version
  2. Employee.prototype = Object.create(Person.prototype);
  3. function Employee(firstName, lastName, age, address, jobTitle, yearStarted) {
  4. Person.call(this, firstName, lastName, age, address);
  5. this.jobTitle = jobTitle;
  6. this.yearStarted = yearStarted;
  7. }
  8. Employee.prototype.describe = function () {
  9. return `I am ${this.getFullName()} and I have a position of ${this.jobTitle} and I started at ${this.yearStarted}`;
  10. }
  11. Employee.prototype.toString = function () {
  12. return "[object Employee]";
  13. }
  14. //ES6 Version
  15. class Employee extends Person { //Inherits from "Person" class
  16. constructor(firstName, lastName, age, address, jobTitle, yearStarted) {
  17. super(firstName, lastName, age, address);
  18. this.jobTitle = jobTitle;
  19. this.yearStarted = yearStarted;
  20. }
  21. describe() {
  22. return `I am ${this.getFullName()} and I have a position of ${this.jobTitle} and I started at ${this.yearStarted}`;
  23. }
  24. toString() { // Overriding the "toString" method of "Person"
  25. return "[object Employee]";
  26. }
  27. }

怎么知道本质上是不是原型呢?

  1. class Something {
  2. }
  3. function AnotherSomething(){
  4. }
  5. const as = new AnotherSomething();
  6. const s = new Something();
  7. console.log(typeof Something); // logs "function"
  8. console.log(typeof AnotherSomething); // logs "function"
  9. console.log(as.toString()); // logs "[object Object]"
  10. console.log(as.toString()); // logs "[object Object]"
  11. console.log(as.toString === Object.prototype.toString);
  12. console.log(s.toString === Object.prototype.toString);
  13. // both logs return true indicating that we are still using
  14. // prototypes under the hoods because the Object.prototype is
  15. // the last part of the Prototype Chain and "Something"
  16. // and "AnotherSomething" both inherit from Object.prototype

45、什么是模板字符串?

模板字符串是JS中构造字符串的新方式。我们可以使用左引号和右引号创建字符串。

  1. //ES5 Version
  2. var greet = 'Hi I\'m Mark';
  3. //ES6 Version
  4. let greet = `Hi I'm Mark`;

在ES5中,为了避免出现',我们不得不用\来转义字符。而在模板字符串里完全不用。

  1. //ES5 Version
  2. var lastWords = '\n'
  3. + ' I \n'
  4. + ' Am \n'
  5. + 'Iron Man \n';
  6. //ES6 Version
  7. let lastWords = `
  8. I
  9. Am
  10. Iron Man
  11. `;

在ES5中, 我们需要用\n来换行。而ES6中不需要。

  1. //ES5 Version
  2. function greet(name) {
  3. return 'Hello ' + name + '!';
  4. }
  5. //ES6 Version
  6. const greet = name => {
  7. return `Hello ${name} !`;
  8. }

在ES5中,如果要添加一个表达式或者值,需要用+或者字符串连接符。在模板字符串中,我们只需要嵌入${expr}既可,不仅比ES5更加简洁。

46、什么是对象解构

对象结构是一种新的方式,从对象或者数组中获取值或者提取值。
假使我们有一个像这样的对象。

  1. const employee = {
  2. firstName: "Marko",
  3. lastName: "Polo",
  4. position: "Software Developer",
  5. yearHired: 2017
  6. };

下面这种获取属性的方式有一些啰嗦,如果我们有一个大对象并且有很多的属性,这种方式就显得令人厌烦。

  1. var firstName = employee.firstName;
  2. var lastName = employee.lastName;
  3. var position = employee.position;
  4. var yearHired = employee.yearHired;

如果我们使用对象结构这种方式,会更简洁和花费更少的时间。对象解构语法针对对象获取属性,我们可以用{},针对数组可以用[]

  1. let { firstName, lastName, position, yearHired } = employee;

如果我们想改变我们要抽取的变量的名称,可以用propertyName:newName语法。

  1. let { firstName: fName, lastName: lName, position, yearHired } = employee;

我们可以在解构的时候设置默认值。在下面的例子中,如果firstName属性返回的是一个undefined值,当我们解构firstName的时候 就会有一个默认的值 ‘Mark’。

  1. let { firstName = "Mark", lastName: lName, position, yearHired } = employee;

47、ES6 模块化是什么?

模块化可以把代码分割到不同的文件里,可使代码可维护。同时避免放入一个大文件中。为了代码的可维护性,在ES6之前有两种流行的模块系统在JS中。

  • CommonJS - NodeJs
  • AMD(Asyn) - Browsers

import 用于从其他文件中导入函数或者值而export则是暴露函数或者值从文件中。
ES5 CommonJS

  1. // Using ES5 CommonJS - helpers.js
  2. exports.isNull = function (val) {
  3. return val === null;
  4. }
  5. exports.isUndefined = function (val) {
  6. return val === undefined;
  7. }
  8. exports.isNullOrUndefined = function (val) {
  9. return exports.isNull(val) || exports.isUndefined(val);
  10. }

ES6模块化

  1. // Using ES6 Modules - helpers.js
  2. export function isNull(val){
  3. return val === null;
  4. }
  5. export function isUndefined(val) {
  6. return val === undefined;
  7. }
  8. export function isNullOrUndefined(val) {
  9. return isNull(val) || isUndefined(val);
  10. }

引入其他文件的函数

  1. // Using ES5 (CommonJS) - index.js
  2. const helpers = require('./helpers.js'); // helpers is an object
  3. const isNull = helpers.isNull;
  4. const isUndefined = helpers.isUndefined;
  5. const isNullOrUndefined = helpers.isNullOrUndefined;
  6. // or if your environment supports Destructuring
  7. const { isNull, isUndefined, isNullOrUndefined } = require('./helpers.js');
  1. // ES6 Modules - index.js
  2. import * as helpers from './helpers.js'; // helpers is an object
  3. // or
  4. import { isNull, isUndefined, isNullOrUndefined as isValid } from './helpers.js';
  5. // using "as" for renaming named exports

导出单个函数或者 默认导出
ES5

  1. // Using ES5 (CommonJS) - index.js
  2. class Helpers {
  3. static isNull(val) {
  4. return val === null;
  5. }
  6. static isUndefined(val) {
  7. return val === undefined;
  8. }
  9. static isNullOrUndefined(val) {
  10. return this.isNull(val) || this.isUndefined(val);
  11. }
  12. }
  13. module.exports = Helpers;

ES6

  1. // Using ES6 Modules - helpers.js
  2. class Helpers {
  3. static isNull(val) {
  4. return val === null;
  5. }
  6. static isUndefined(val) {
  7. return val === undefined;
  8. }
  9. static isNullOrUndefined(val) {
  10. return this.isNull(val) || this.isUndefined(val);
  11. }
  12. }
  13. export default Helpers

引入单个函数从其他文件里。

  1. // Using ES5 (CommonJS) - index.js
  2. const Helpers = require('./helpers.js');
  3. console.log(Helpers.isNull(null));
  1. import Helpers from '.helpers.js'
  2. console.log(Helpers.isNull(null));

48、什么是Set对象,怎么使用它。

Set是ES6的对象,可以让你存储唯一值,基础类型 或者对象引用。一个值在Set里只能存在一次。他会检查值是否已存在于Set里,用SameValueZero算法。

我们可以用Set构造函数来创建Set实例。也可以传递一个可迭代对象作为初始值。

  1. const set1 = new Set();
  2. const set2 = new Set(["a","b","c","d","d","e"]);

可以通过add方法添加新的值。

  1. set2.add("f");
  2. set2.add("g").add("h").add("i").add("j").add("k").add("k");
  3. // the last "k" will not be added to the set object because it already exists

通过delete删除一个值。

  1. set2.delete("k") // returns true because "k" exists in the set object
  2. set2.delete("z") // returns false because "z" does not exists in the set object

has方法检查一个特定的值是否存在。

  1. set2.has("a") // returns true because "a" exists in the set object
  2. set2.has("z") // returns false because "z" does not exists in the set object

可以通过size属性返回Set的长度。

  1. set2.size // returns 10

可用clear清楚所有的元素。

  1. set2.clear(); // clears the set data

可用Set清除数组里的重复值。

  1. const numbers = [1, 2, 3, 4, 5, 6, 6, 7, 8, 8, 5];
  2. const uniqueNums = [...new Set(numbers)]; // has a value of [1,2,3,4,5,6,7,8]

49、什么是回调函数?

回调函数是一个函数 ,会在之后一个时间点调用的函数。

  1. const btnAdd = document.getElementById('btnAdd');
  2. btnAdd.addEventListener('click', function clickCallback(e) {
  3. // do something useless
  4. });

在这个例子上,我们等待click event事件的发生。当点击后,clickCallback函数就会执行。回调函数会给一个数据或者事件添加一些功能。reducefiltermap方法期望一个回调函数作为参数。一个好的比喻就是当你打给某人他们没有应答,你留了口信并且期望他们callback。打电话和留口信就是事件或者数据,而callback就是你期望后续发生的动作。

50、什么是Promise

Promise是JS中处理异步操作的一种方式。它代表着异步操作的值。Promises用来解决处理和解决异步代码,在这之前用的是回调函数。

  1. fs.readFile('somefile.txt', function (e, data) {
  2. if (e) {
  3. console.log(e);
  4. }
  5. console.log(data);
  6. });

回调函数会产生回调地狱问题。

  1. //Callback Hell yucksss
  2. fs.readFile('somefile.txt', function (e, data) {
  3. //your code here
  4. fs.readdir('directory', function (e, files) {
  5. //your code here
  6. fs.mkdir('directory', function (e) {
  7. //your code here
  8. })
  9. })
  10. })

如果用promsie 会可读和可维护。

  1. promReadFile('file/path')
  2. .then(data => {
  3. return promReaddir('directory');
  4. })
  5. .then(data => {
  6. return promMkdir('directory');
  7. })
  8. .catch(e => {
  9. console.log(e);
  10. })

Promise有三种状态
**Pending** - 初始状态。Promise的结果未知,因为promise的执行还没有完成。
**FulFilled** - 异步操作完成,并且有返回值。
**Rejected** - 异步操作失败,并且有返回原因为什么失败。
Settled - promise 或者fulfilled 或者rejected

如果异步操作完成了,并且没有错误就会调用resolve函数,如果有错误发生,就调用Reject函数,并且返回错误原因。在.then方法访问fulfilled promise的结果,在.catch方法里捕获错误。我们可以在.then方法里链接多个promise 操作,因为.then方法返回promise。

  1. const myPromiseAsync = (...args) => {
  2. return new Promise((resolve, reject) => {
  3. doSomeAsync(...args, (error, data) => {
  4. if (error) {
  5. reject(error);
  6. } else {
  7. resolve(data);
  8. }
  9. })
  10. })
  11. }
  12. myPromiseAsync()
  13. .then(result => {
  14. console.log(result);
  15. })
  16. .catch(reason => {
  17. console.log(reason);
  18. })

我们可以创建一个helper函数,把一个回调函数转成promise函数。就像**promisify**一样。

  1. const toPromise = (asyncFuncWithCallback) => {
  2. return (...args) => {
  3. return new Promise((res, rej) => {
  4. asyncFuncWithCallback(...args, (e, result) => {
  5. return e ? rej(e) : res(result);
  6. });
  7. });
  8. }
  9. }
  10. const promReadFile = toPromise(fs.readFile);
  11. promReadFile('file/path')
  12. .then((data) => {
  13. console.log(data);
  14. })
  15. .catch(e => console.log(e));

51、什么是async/await ,它是怎么工作的?

async/await 是JS中写异步或者不阻塞代码的新方式。它是基于Promise的。相比Promise和回调函数,它使异步代码更加可读和简洁。前提是你要学习下Promise的知识,本质上还是Promise。
Promise的方式:

  1. function callApi() {
  2. return fetch("url/to/api/endpoint")
  3. .then(resp => resp.json())
  4. .then(data => {
  5. //do something with "data"
  6. }).catch(err => {
  7. //do something with "err"
  8. });
  9. }

async/await的方式
注意:这里我们用try/catch的方式来捕获错误,一旦异步操作有任何的异常发生。

  1. async function callApi() {
  2. try {
  3. const resp = await fetch("url/to/api/endpoint");
  4. const data = await resp.json();
  5. //do something with "data"
  6. } catch (e) {
  7. //do something with "err"
  8. }
  9. }

注意:在函数声明前面添加async关键字,使得函数隐式的返回Promise。

  1. const giveMeOne = async () => 1;
  2. giveMeOne()
  3. .then((num) => {
  4. console.log(num); // logs 1
  5. });

注意:await关键字只能用在async函数内部。使用await关键字而不在async函数内部,会抛出异常。
await关键字会等待右手边的表达式(大部分情况下是Promise)返回在执行下一行代码之前。

  1. const giveMeOne = async () => 1;
  2. function getOne() {
  3. try {
  4. const num = await giveMeOne();
  5. console.log(num);
  6. } catch (e) {
  7. console.log(e);
  8. }
  9. }
  10. //Throws a Compile-Time Error = Uncaught SyntaxError: await is only valid in an async function
  11. async function getTwo() {
  12. try {
  13. const num1 = await giveMeOne();
  14. //finishes this async operation first before going to
  15. const num2 = await giveMeOne(); //this line
  16. return num1 + num2;
  17. } catch (e) {
  18. console.log(e);
  19. }
  20. }
  21. await getTwo(); // returns 2

52、Spread运算符和Rest运算符的区别?

Spread操作符和Rest参数拥有同样的操作符...,两者的区别是Spread操作符是我们给与或者扩展数组中的单个数据到另外的数据,而Rest参数用于函数中或者一个数组用于获取所有的参数或者值 并把他们放到新的数组或者提取他们中的部分。

  1. function add(a, b) {
  2. return a + b;
  3. };
  4. const nums = [5, 6];
  5. const sum = add(...nums);
  6. console.log(sum);

在这个例子里,我们使用了Spread操作符,当调用函数add,扩展了nums数组。所以参数a的值就是5b的值就是6, 那么结果就是11

  1. function add(...rest) {
  2. return rest.reduce((total,current) => total + current);
  3. };
  4. console.log(add(1, 2)); // logs 3
  5. console.log(add(1, 2, 3, 4, 5)); // logs 15

在这个例子里,add函数可以接受任意数量的参数,并且把他们相加返回总数。

  1. const [first, ...others] = [1, 2, 3, 4, 5];
  2. console.log(first); //logs 1
  3. console.log(others); //logs [2,3,4,5]

在这个例子里,我们用Rest操作符来提取剩余的数组中的值, 并把他们放入新的数组除了第一项。

52、什么是默认参数?

默认参数是JS中定义默认变量的新方式,在ES6和ES2005版本可用。

  1. //ES5 Version
  2. function add(a,b){
  3. a = a || 0;
  4. b = b || 0;
  5. return a + b;
  6. }
  7. //ES6 Version
  8. function add(a = 0, b = 0){
  9. return a + b;
  10. }
  11. //If we don't pass any argument for 'a' or 'b' then
  12. // it's gonna use the "default parameter" value which is 0
  13. add(1); // returns 1

我们也可以使用结构针对默认参数。

  1. function getFirst([first, ...rest] = [0, 1]) {
  2. return first;
  3. }
  4. getFirst(); // returns 0
  5. getFirst([10,20,30]); // returns 10
  6. function getArr({ nums } = { nums: [1, 2, 3, 4] }){
  7. return nums;
  8. }
  9. getArr(); // returns [1, 2, 3, 4]
  10. getArr({nums:[5,4,3,2,1]}); // returns [5,4,3,2,1]

我们也可以先定义参数,然后在后面的参数中使用先前定义的参数。

  1. function doSomethingWithValue(value = "Hello World",
  2. callback = () => { console.log(value) }) {
  3. callback();
  4. }
  5. doSomethingWithValue(); //logs "Hello World"

54、什么是包装对象?

基本数据类型 stringnumberboolean除了nullundefined,也有属性和方法,尽管他们不是objects

  1. let name = "marko";
  2. console.log(typeof name); // logs "string"
  3. console.log(name.toUpperCase()); // logs "MARKO"

如上,name是基本数据类型 string 没有属性和方法,在这个例子调用toUpperCase方法,并且返回MARKO。
原因是对于基本类型的值会强制转换成object,所以name变量就拥有了object的行为。所有的基本数据类型除了nullundefined都有包装对象。包装对象String ,Number,BooleanSymbolBigIntname.toUpperCase()的调用如下所示:

  1. console.log(new String(name).toUpperCase()); // logs "MARKO"

新创建的对象会被立即释放,在访问属性或者调用方法后。

55、隐式和显示类型转换的区别?

隐式类型转换是把一个值转换成另一种类型的方式,不需要手动操作。

  1. console.log(1 + '6');
  2. console.log(false + true);
  3. console.log(6 * '2');

上面第一个打印16。在其他编程语言这可能抛出编译错误,但是在JS中,会把1转换成string,在莲字+操作符的作用下。我们不需要做任何事情,JS会自动的帮我们处理。

第二个打印1,首先把false转成boolean结果是0, 而true的转换结果是1 所以最终结果是1

第三个打印结果是12,把2转成number,并且计算2*6,最终结果是12。

JS类型转换规则

显示类型转换是显示的把值转换成另一种类型。

  1. console.log(1 + parseInt('6'));

在这个例子中,我们用parseInt显示的把6转成number,只有+操作符计算 1+6的值。

56、什么是NaN?如何检查是不是NaN?

NaN 是 ‘Not a Number’,这是在转换或者执行操作把一个number到一个非number的值时,结果是NaN

  1. let a;
  2. console.log(parseInt('abc'));
  3. console.log(parseInt(null));
  4. console.log(parseInt(undefined));
  5. console.log(parseInt(++a));
  6. console.log(parseInt({} * 10));
  7. console.log(parseInt('abc' - 2));
  8. console.log(parseInt(0 / 0));
  9. console.log(parseInt('10a' * 10));

JS中有内置方法isNaN,用来检查值是不是isNaN。但是这个函数也有奇怪的行为。

  1. console.log(isNaN()); //logs true
  2. console.log(isNaN(undefined)); //logs true
  3. console.log(isNaN({})); //logs true
  4. console.log(isNaN(String('a'))); //logs true
  5. console.log(isNaN(() => { })); //logs true

上面所有的这些都返回true,即使有些值并不是NaN
在ES6或者ES2005, 我们推荐用Number.isNaN方法来检查这个值是不是NaN,我们可以用下面这种,因为NaN是JS中唯一不等于它本身的值。

  1. function checkIfNaN(value) {
  2. return value !== value;
  3. }

57、如何检查是不是数组?

对于是不是数组,我们可以用Array.isArray方法来检查。

  1. console.log(Array.isArray(5)); //logs false
  2. console.log(Array.isArray("")); //logs false
  3. console.log(Array.isArray()); //logs false
  4. console.log(Array.isArray(null)); //logs false
  5. console.log(Array.isArray({ length: 5 })); //logs false
  6. console.log(Array.isArray([])); //logs true

如果环境不支持上述方法,可以用下列方法。

  1. function isArray(value){
  2. return Object.prototype.toString.call(value) === "[object Array]"
  3. }

58、如果检查是不是奇数在不使用%和取模操作符?

可以用按位操作符&来解决这个问题。

  1. function isEven(num) {
  2. if (num & 1) {
  3. return false;
  4. } else {
  5. return true;
  6. }
  7. };

0的二进制是000;
1的二进制是 001;
2的二进制是010;
3的二进制是011;
4的二进制是100;
5的二进制是101;
6是110;
7是111;
。。。。。

a b a&b
0 0 0
0 1 0
1 0 0
1 1 1

当我们执行表达式5&1返回 1;首先&可以把两个number转成二进制,5是101,1是001;
然后再用按位AND操作符比较每一位。101001

101 & 001
101
001
001
  • 首先从最左边开始比较,10,结果是0;
  • 在比较中间位置,00 ,结果是0;
  • 最后比较最右边,11 ,结果是1;
  • 最后,001转换成十进制就是 1

表达式4&1将会返回0
下面的方式 ,可以通过递归的方式解决这个问题。

  1. function isEven(num) {
  2. if (num < 0 || num === 1) return false;
  3. if (num == 0) return true;
  4. return isEven(num - 2);
  5. }

59、如何检查一个特定的属性是不是在对象上?

3种方式可以做到。
首先,可以用in操作符。语法是 propertyname in obj,存在就返回true,不存在就返回false

  1. const o = {
  2. "prop" : "bwahahah",
  3. "prop2" : "hweasa"
  4. };
  5. console.log("prop" in o); //This logs true indicating the property "prop" is in "o" object
  6. console.log("prop1" in o); //This logs false indicating the property "prop" is not in "o" object

第二,可以用对象上的hasOwnProperty方法。如果存在返回true,不存在返回false。

  1. //Still using the o object in the first example.
  2. console.log(o.hasOwnProperty("prop2")); // This logs true
  3. console.log(o.hasOwnProperty("prop1")); // This logs false

可以用括号表达式obj["prop"],如果存在就返回值,不存在就返回undefined

  1. //Still using the o object in the first example.
  2. console.log(o["prop"]); // This logs "bwahahah"
  3. console.log(o["prop1"]); // This logs undefined

60、什么是AJAX

AJAX 是 Asynchronous JavaScript and XML。这是一组相关的技术用来显示数据。这意味着我们可以发送或者获取数据来自服务器而不需要重刷页面。
相关技术:

  • HTML - 网页页面结构
  • CSS - 网页样式
  • JavaScript - 页面行为,或者更新DOM的行为
  • XMLHttpRequest API - 用于从服务器 抽取和发送数据
  • PHP,Python,Nodejs - 服务端语言

61、JS中创建对象的方式?

对象字面量

  1. const o = {
  2. name: "Mark",
  3. greeting() {
  4. return `Hi, I'm ${this.name}`;
  5. }
  6. };
  7. o.greeting(); //returns "Hi, I'm Mark"

构造函数

  1. function Person(name) {
  2. this.name = name;
  3. }
  4. Person.prototype.greeting = function () {
  5. return `Hi, I'm ${this.name}`;
  6. }
  7. const mark = new Person("Mark");
  8. mark.greeting(); //returns "Hi, I'm Mark"

object.create()方法。

  1. const n = {
  2. greeting() {
  3. return `Hi, I'm ${this.name}`;
  4. }
  5. };
  6. const o = Object.create(n); // sets the prototype of "o" to be "n"
  7. o.name = "Mark";
  8. console.log(o.greeting()); // logs "Hi, I'm Mark"

62、Object.seal()Object.freeze()方法的不同?

对于Object.freeze(),对象的属性是不能更改的,这意味着我们不能改变和编辑这些属性值。
对于Object.seal,我们可以更改存在的属性,但是不能添加新的属性。

63、in操作符和hasOwnProperty方法的区别

首先。两者都可以用于检查对象的属性是否存在,存在就返回true,否则返回false。
两者的区别在于 in操作符会检查对象的原型链,而hasOwnProperty只会检查当前对象,不会检查原型链。

  1. // We'll still use the object in the previous question.
  2. console.log("prop" in o); // This logs true;
  3. console.log("toString" in o); // This logs true, the toString method is available in this object's prototype which is the Object.prototype
  4. console.log(o.hasOwnProperty("prop")); // This logs true
  5. console.log(o.hasOwnProperty("toString")); // This logs false, does not check the object's prototype

64、JS中处理异步代码的方式?

  • 回调函数
  • Promise
  • async/await
  • 异步库例如 co, q, bluebirdasync.js

65、函数表达式和函数声明的区别?

  1. hoistedFunc();
  2. notHoistedFunc();
  3. function hoistedFunc(){
  4. console.log("I am hoisted");
  5. }
  6. var notHoistedFunc = function(){
  7. console.log("I will not be hoisted!");
  8. }

这里notHoistedFunc调用会抛出异常,而hoistedFunc则不会;这是因为hoistedFunc会提升而notHoistedFunc不会。

66、函数被调用的方式?

在JS中,函数被调用有4中方式。调用方式决定这this的值和函数的拥有者对象。

  • 作为函数调用 - 如果一个函数不是作为方法被调用,而是作为一个构造函数 或者 applycall那么就可以说它是作为函数被调用。函数的拥有者对象就是 window对象。

    1. //Global Scope
    2. function add(a,b){
    3. console.log(this);
    4. return a + b;
    5. }
    6. add(1,5); // logs the "window" object and returns 6
    7. const o = {
    8. method(callback){
    9. callback();
    10. }
    11. }
    12. o.method(function (){
    13. console.log(this); // logs the "window" object
    14. });
  • 作为方法被调用 - 如果对象的属性值是一个函数,那么这个就是对象的方法。当这个方法被调用的时候,**this**值就是该对象。

    1. const details = {
    2. name : "Marko",
    3. getName(){
    4. return this.name;
    5. }
    6. }
    7. details.getName(); // returns Marko
    8. // the "this" value inside "getName" method will be the "details" object
  • 作为构造函数被调用 - 如果一个函数被用 new关键字来调用,那么就可以叫做 function constructor,一个空对象将会被创建,this将会指向他。 ```javascript function Employee(name, position, yearHired) { // creates an empty object {} // then assigns the empty object to the “this” keyword // this = {}; this.name = name; this.position = position; this.yearHired = yearHired; // inherits from Employee.prototype // returns the “this” value implicitly if no // explicit return statement is specified };

const emp = new Employee(“Marko Polo”, “Software Developer”, 2017);

  1. - `apply``call`调用。如果想显示的指定`this`的值,后者函数的拥有者对象,可以用这个方法。
  2. ```javascript
  3. const obj1 = {
  4. result:0
  5. };
  6. const obj2 = {
  7. result:0
  8. };
  9. function reduceAdd(){
  10. let result = 0;
  11. for(let i = 0, len = arguments.length; i < len; i++){
  12. result += arguments[i];
  13. }
  14. this.result = result;
  15. }
  16. reduceAdd.apply(obj1, [1, 2, 3, 4, 5]);
  17. //the "this" object inside the "reduceAdd" function will be "obj1"
  18. reduceAdd.call(obj2, 1, 2, 3, 4, 5);
  19. //the "this" object inside the "reduceAdd" function will be "obj2"

67、什么是memoization,用它做什么?

memoization是一个构建函数的过程,能够记忆之前计算的结果或者值。
_memoization_函数可以避免重复计算。这可以节约很多时间,但同时为了保存之前的值也消耗了很多的内存。

68、实现一个缓存帮助函数

  1. function memoize(fn) {
  2. const cache = {};
  3. return function (param) {
  4. if (cache[param]) {
  5. console.log('cached');
  6. return cache[param];
  7. } else {
  8. let result = fn(param);
  9. cache[param] = result;
  10. console.log(`not cached`);
  11. return result;
  12. }
  13. }
  14. }
  15. const toUpper = (str ="")=> str.toUpperCase();
  16. const toUpperMemoized = memoize(toUpper);
  17. toUpperMemoized("abcdef");
  18. toUpperMemoized("abcdef");

下面的函数可以接受多参数。

  1. const slice = Array.prototype.slice;
  2. function memoize(fn) {
  3. const cache = {};
  4. return (...args) => {
  5. const params = slice.call(args);
  6. console.log(params);
  7. if (cache[params]) {
  8. console.log('cached');
  9. return cache[params];
  10. } else {
  11. let result = fn(...args);
  12. cache[params] = result;
  13. console.log(`not cached`);
  14. return result;
  15. }
  16. }
  17. }
  18. const makeFullName = (fName, lName) => `${fName} ${lName}`;
  19. const reduceAdd = (numbers, startingValue = 0) => numbers.reduce((total, cur) => total + cur, startingValue);
  20. const memoizedMakeFullName = memoize(makeFullName);
  21. const memoizedReduceAdd = memoize(reduceAdd);
  22. memoizedMakeFullName("Marko", "Polo");
  23. memoizedMakeFullName("Marko", "Polo");
  24. memoizedReduceAdd([1, 2, 3, 4, 5], 5);
  25. memoizedReduceAdd([1, 2, 3, 4, 5], 5);

69、为什么typeof null 返回object,如何检查一个值是不是null

typeof null == 'object'这个表达式会永远返回true,原因是自从JS诞生是null的实现方式。曾经有一个提议,typeof null == 'object'改成 typeof == 'null'但是被拒绝了,这会导致更多的bug。
可以用 ===和严格相等操作符来比较。

  1. function isNull(value){
  2. return value === null;
  3. }

70、new 关键字做了什么?

new关键字 用于在构造函数中创建对象。
假使我们有如下示例:

  1. function Employee(name, position, yearHired) {
  2. this.name = name;
  3. this.position = position;
  4. this.yearHired = yearHired;
  5. };
  6. const emp = new Employee("Marko Polo", "Software Developer", 2017);

new关键字会做4件事情。

  • 创建一个空对象
  • 把这个空对象分配给this
  • 函数会继承 functionName.prototype
  • 如果没有指定的return返回,就返回this

上面的示例中,首先创建空对象{}。然后赋值给this,this = {},并且给this对象添加属性。
由于没有显示的return语句,就自动返回this值。