06月24, 2022

二、JavaScript 面试题汇总(51-100)

51. 原型和原型链?(字节)

参考答案:

请参阅前面第 34 题答案。

52. 排序算法---(时间复杂度、空间复杂度)

参考答案: 关于排序可以参考我的java相关排序文章:http://www.yanhongzhi.com/post/sort10.html

算法(Algorithm)是指用来操作数据、解决程序问题的一组方法。对于同一个问题,使用不同的算法,也许最终得到的结果是一样的,但在过程中消耗的资源和时间却会有很大的区别。

主要还是从算法所占用的「时间」和「空间」两个维度去考量。

  • 时间维度:是指执行当前算法所消耗的时间,我们通常用「时间复杂度」来描述。
  • 空间维度:是指执行当前算法需要占用多少内存空间,我们通常用「空间复杂度」来描述。

因此,评价一个算法的效率主要是看它的时间复杂度和空间复杂度情况。然而,有的时候时间和空间却又是「鱼和熊掌」,不可兼得的,那么我们就需要从中去取一个平衡点。

排序也称排序算法(Sort Algorithm),排序是将一组数据,依指定的顺序进行排列的过程

排序的分类分为内部排序外部排序法

  • 内部排序:指将需要处理的所有数据都加载到内部存储器(内存)中进行排序。
  • 外部排序:数据量过大,无法全部加载到内存中,需要借助外部存储(文件等)进行排序。

53. 浏览器事件循环和 node 事件循环(搜狗)

参考答案:

  1. 浏览器中的 Event Loop

事件循环中的异步队列有两种:macro(宏任务)队列和 micro(微任务)队列。宏任务队列可以有多个,微任务队列只有一个

  • 常见的 macro-task 比如:setTimeout、setInterval、 setImmediate、script(整体代码)、 I/O 操作、UI 渲染等。
  • 常见的 micro-task 比如: process.nextTick、new Promise( ).then(回调)、MutationObserver(html5 新特性) 等。

当某个宏任务执行完后,会查看是否有微任务队列。如果有,先执行微任务队列中的所有任务,如果没有,会读取宏任务队列中排在最前的任务,执行宏任务的过程中,遇到微任务,依次加入微任务队列。栈空后,再次读取微任务队列里的任务,依次类推。

  1. Node 中的事件循环

Node 中的 Event Loop 和浏览器中的是完全不相同的东西。Node.js 采用 V8 作为 js 的解析引擎,而 I/O 处理方面使用了自己设计的 libuvlibuv 是一个基于事件驱动的跨平台抽象层,封装了不同操作系统一些底层特性,对外提供统一的 API,事件循环机制也是它里面的实现。

Node.JS 的事件循环分为 6 个阶段:

  • timers 阶段:这个阶段执行 timersetTimeout、setInterval )的回调
  • I/O callbacks 阶段:处理一些上一轮循环中的少数未执行的 I/O 回调
  • idle、prepare 阶段:仅 Node.js 内部使用
  • poll 阶段:获取新的 I/O 事件, 适当的条件下 Node.js 将阻塞在这里
  • check 阶段:执行 setImmediate( ) 的回调
  • close callbacks 阶段:执行 socketclose 事件回调

Node.js 的运行机制如下:

  • V8 引擎解析 JavaScript 脚本。
  • 解析后的代码,调用 Node API
  • libuv 库负责 Node API 的执行。它将不同的任务分配给不同的线程,形成一个 Event Loop(事件循环),以异步的方式将任务的执行结果返回给 V8 引擎。
  • V8 引擎再将结果返回给用户。

54. 闭包的好处

参考答案:

请参阅前面第 20 题以及第 36 题答案。

55. let、const、var 的区别

参考答案:

  1. var 定义的变量,没有块的概念,可以跨块访问, 不能跨函数访问,有变量提升。
  2. let 定义的变量,只能在块作用域里访问,不能跨块访问,也不能跨函数访问,无变量提升,不可以重复声明。
  3. const 用来定义常量,使用时必须初始化(即必须赋值),只能在块作用域里访问,而且不能修改,无变量提升,不可以重复声明。

56. 闭包、作用域(可以扩充到作用域链)

参考答案:

什么是作业域?

ES5 中只存在两种作用域:全局作用域和函数作用域。在 JavaScript 中,我们将作用域定义为一套规则,这套规则用来管理引擎如何在当前作用域以及嵌套子作用域中根据标识符名称进行变量(变量名或者函数名)查找。

什么是作用域链?

当访问一个变量时,编译器在执行这段代码时,会首先从当前的作用域中查找是否有这个标识符,如果没有找到,就会去父作用域查找,如果父作用域还没找到继续向上查找,直到全局作用域为止,,而作用域链,就是有当前作用域与上层作用域的一系列变量对象组成,它保证了当前执行的作用域对符合访问权限的变量和函数的有序访问。

闭包产生的本质

当前环境中存在指向父级作用域的引用

什么是闭包

闭包是一种特殊的对象,它由两部分组成:执行上下文(代号 A),以及在该执行上下文中创建的函数 (代号 B),当 B 执行时,如果访问了 A 中变量对象的值,那么闭包就会产生,且在 Chrome 中使用这个执行上下文 A 的函数名代指闭包。

一般如何产生闭包

  • 返回函数
  • 函数当做参数传递

闭包的应用场景

  • 柯里化 bind
  • 模块

57. Promise

参考答案:

Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理且更强大。它最早由社区提出并实现,ES6将其写进了语言标准,统一了用法,并原生提供了Promise对象。

特点

  1. 对象的状态不受外界影响 (3 种状态)

    • Pending 状态(进行中)

    • Fulfilled 状态(已成功)

    • Rejected 状态(已失败)
  2. 一旦状态改变就不会再变 (两种状态改变:成功或失败)

    • Pending -> Fulfilled
    • Pending -> Rejected

用法

var promise = new Promise(function(resolve, reject){
    // ... some code

    if (/* 异步操作成功 */) {
        resolve(value);
    } else {
        reject(error);
    }
})

58. 实现一个函数,对一个url进行请求,失败就再次请求,超过最大次数就走失败回调,任何一次成功都走成功回调

参考答案:

示例代码如下:

/**
 @params url: 请求接口地址;
 @params body: 设置的请求体;
 @params succ: 请求成功后的回调
 @params error: 请求失败后的回调
 @params maxCount: 设置请求的数量
*/
function request(url, body, succ, error, maxCount = 5) {
 return fetch(url, body)
     .then(res => succ(res))
     .catch(err => {
         if (maxCount <= 0) return error('请求超时');
         return request(url, body, succ, error, --maxCount);
     });
}

// 调用请求函数
request('https://java.some.com/pc/reqCount', { method: 'GET', headers: {} },
 (res) => {
     console.log(res.data);
 },
 (err) => {
     console.log(err);
 })

59. 冒泡排序

参考答案:

冒泡排序的核心思想是:

  1. 比较相邻的两个元素,如果前一个比后一个大或者小(取决于排序的顺序是小到大还是大到小),则交换位置。
  2. 比较完第一轮的时候,最后一个元素是最大或最小的元素。
  3. 这时候最后一个元素已经是最大或最小的了,所以下一次冒泡的时候最后一个元素不需要参与比较。

示例代码:

function bSort(arr) {
    var len = arr.length;
    // 外层 for 循环控制冒泡的次数
    for (var i = 0; i < len - 1; i++) {
        for (var j = 0; j < len - 1 - i; j++) {
            // 内层 for 循环控制每一次冒泡需要比较的次数
            // 因为之后每一次冒泡的两两比较次数会越来越少,所以 -i
            if (arr[j] > arr[j + 1]) {
                var temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
    return arr;
}

//举个数组
myArr = [20, -1, 27, -7, 35];
//使用函数
console.log(bSort(myArr)); // [ -7, -1, 20, 27, 35 ]

60. 数组降维

参考答案:

数组降维就是将一个嵌套多层的数组进行降维操作,也就是对数组进行扁平化。在 ES5 时代我们需要自己手写方法或者借助函数库来完成,但是现在可以使用 ES6 新提供的数组方法 flat 来完成数组降维操作。

解析:使用 flat 方法会接收一个参数,这个参数是数值类型,是要处理扁平化数组的深度,生成后的新数组是独立存在的,不会对原数组产生影响。

flat 方法的语法如下:

var newArray = arr.flat([depth])

其中 depth 指定要提取嵌套数组结构的深度,默认值为 1

示例如下:

var arr = [1, 2, [3, 4, [5, 6]]];
console.log(arr.flat());      // [1, 2, 3, 4, [5, 6]]
console.log(arr.flat(2));     // [1, 2, 3, 4, 5, 6]

上面的代码定义了一个层嵌套的数组,默认情况下只会拍平一层数组,也就是把原来的三维数组降低到了二维数组。在传入的参数为 2 时,则会降低两维,成为一个一维数组。

使用 Infinity,可展开任意深度的嵌套数组,示例如下:

var arr = [1, 2, [3, 4, [5, 6, [7, 8]]]];
console.log(arr.flat(Infinity));  // [1, 2, 3, 4, 5, 6, 7, 8]

在数组中有空项的时候,使用 flat 方法会将中的空项进行移除。

var arr = [1, 2, , 4, 5];
console.log(arr.flat()); // [1, 2, 4, 5]

上面的代码中,数组中第三项是空值,在使用 flat 后会对空项进行移除。

61. call apply bind

参考答案:

请参阅前面第 11 题答案。

62. promise 代码题

new Promise((resolve, reject) => {
    reject(1);
    console.log(2);
    resolve(3);
    console.log(4);
}).then((res) => { console.log(res) })
    .catch(res => { console.log('reject1') })
try {
    new Promise((resolve, reject) => {
        throw 'error'
    }).then((res) => { console.log(res) })
        .catch(res => { console.log('reject2') })
} catch (err) {
    console.log(err)
}

参考答案:

2 4 reject1 reject2

直播课或者录播课进行解析。

63. proxy 是实现代理,可以改变 js 底层的实现方式, 然后说了一下和 Object.defineProperty 的区别

参考答案:

两者的区别总结如下:

  • 代理原理:Object.defineProperty的原理是通过将数据属性转变为存取器属性的方式实现的属性读写代理。而Proxy则是因为这个内置的Proxy对象内部有一套监听机制,在传入handler对象作为参数构造代理对象后,一旦代理对象的某个操作触发,就会进入handler中对应注册的处理函数,此时我们就可以有选择的使用Reflect将操作转发被代理对象上。
  • 代理局限性:Object.defineProperty始终还是局限于属性层面的读写代理,对于对象层面以及属性的其它操作代理它都无法实现。鉴于此,由于数组对象push、pop等方法的存在,它对于数组元素的读写代理实现的并不完全。而使用Proxy则可以很方便的监视数组操作。
  • 自我代理:Object.defineProperty方式可以代理到自身(代理之后使用对象本身即可),也可以代理到别的对象身上(代理之后需要使用代理对象)。Proxy方式只能代理到Proxy实例对象上。这一点在其它说法中是Proxy对象不需要侵入对象就可以实现代理,实际上Object.defineProperty方式也可以不侵入。

64. 使用 ES5ES6 分别实现继承

参考答案:

如果是使用 ES5 来实现继承,那么现在的最优解是使用圣杯模式。圣杯模式的核心思想就是不通过调用父类构造函数来给子类原型赋值,而是取得父类原型的一个副本,然后将返回的新对象赋值给子类原型。具体代码可以参阅前面第 9 题的解析。

ES6 新增了 extends 关键字,直接使用该关键字就能够实现继承。

65. 深拷贝

参考答案:

有深拷贝就有浅拷贝。

浅拷贝就是只拷贝对象的引用,而不深层次的拷贝对象的值,多个对象指向堆内存中的同一对象,任何一个修改都会使得所有对象的值修改,因为它们共用一条数据。

深拷贝不是单纯的拷贝一份引用数据类型的引用地址,而是将引用类型的值全部拷贝一份,形成一个新的引用类型,这样就不会发生引用错乱的问题,使得我们可以多次使用同样的数据,而不用担心数据之间会起冲突。

解析:

「深拷贝」就是在拷贝数据的时候,将数据的所有引用结构都拷贝一份。简单的说就是,在内存中存在两个数据结构完全相同又相互独立的数据,将引用型类型进行复制,而不是只复制其引用关系。

分析下怎么做「深拷贝」:

  1. 首先假设深拷贝这个方法已经完成,为 deepClone
  2. 要拷贝一个数据,我们肯定要去遍历它的属性,如果这个对象的属性仍是对象,继续使用这个方法,如此往复
function deepClone(o1, o2) {
    for (let k in o2) {
        if (typeof o2[k] === 'object') {
            o1[k] = {};
            deepClone(o1[k], o2[k]);
        } else {
            o1[k] = o2[k];
        }
    }
}
// 测试用例
let obj = {
    a: 1,
    b: [1, 2, 3],
    c: {}
};
let emptyObj = Object.create(null);
deepClone(emptyObj, obj);
console.log(emptyObj.a == obj.a);
console.log(emptyObj.b == obj.b);

递归容易造成爆栈,尾部调用可以解决递归的这个问题,ChromeV8 引擎做了尾部调用优化,我们在写代码的时候也要注意尾部调用写法。递归的爆栈问题可以通过将递归改写成枚举的方式来解决,就是通过 for 或者 while 来代替递归。

66. asyncawait 的作用

参考答案:

async 是一个修饰符,async 定义的函数会默认的返回一个 Promise 对象 resolve 的值,因此对 async 函数可以直接进行 then 操作,返回的值即为 then 方法的传入函数。

await 关键字只能放在 async 函数内部, await 关键字的作用就是获取 Promise 中返回的内容, 获取的是 Promise 函数中 resolve 或者 reject 的值。

67. 数据的基础类型(原始类型)有哪些

参考答案:

JavaScript 中的基础数据类型,一共有 7 种:

string,symbol,number,boolean,undefined,nullbigInt

68. typeof null 返回结果

参考答案:

返回 object

解析:至于为什么会返回 object,这实际上是来源于 JavaScript 从第一个版本开始时的一个 bug,并且这个 bug 无法被修复。修复会破坏现有的代码。

原理这是这样的,不同的对象在底层都表现为二进制,在 JavaScript 中二进制前三位都为 0 的话会被判断为 object 类型,null 的二进制全部为 0,自然前三位也是 0,所以执行 typeof 值会返回 object

69. 对变量进行类型判断的方式有哪些

参考答案:

常用的方法有 4 种:

  1. typeof

typeof 是一个操作符,其右侧跟一个一元表达式,并返回这个表达式的数据类型。返回的结果用该类型的字符串(全小写字母)形式表示,包括以下 7 种:number、boolean、symbol、string、object、undefined、function 等。

  1. instanceof

instanceof 是用来判断 A 是否为 B 的实例,表达式为:A instanceof B,如果 AB 的实例,则返回 true,否则返回 false。 在这里需要特别注意的是:instanceof 检测的是原型。

  1. constructor

当一个函数 F 被定义时,JS 引擎会为 F 添加 prototype 原型,然后再在 prototype 上添加一个 constructor 属性,并让其指向 F 的引用。

  1. toString

toString( )Object 的原型方法,调用该方法,默认返回当前对象的 [[Class]] 。这是一个内部属性,其格式为 [object Xxx] ,其中 Xxx 就是对象的类型。

对于 Object 对象,直接调用 toString( ) 就能返回 [object Object] 。而对于其他对象,则需要通过 call / apply 来调用才能返回正确的类型信息。例如:

Object.prototype.toString.call('') ;  // [object String]
Object.prototype.toString.call(1) ;   // [object Number]
Object.prototype.toString.call(true) ;// [object Boolean]
Object.prototype.toString.call(Symbol());//[object Symbol]
Object.prototype.toString.call(undefined) ;// [object Undefined]
Object.prototype.toString.call(null) ;// [object Null]

70. typeofinstanceof 的区别? instanceof 是如何实现?

参考答案:

  1. typeof

typeof 是一个操作符,其右侧跟一个一元表达式,并返回这个表达式的数据类型。返回的结果用该类型的字符串(全小写字母)形式表示,包括以下 7 种:number、boolean、symbol、string、object、undefined、function 等。

  1. instanceof

instanceof 是用来判断 A 是否为 B 的实例,表达式为:A instanceof B,如果 AB 的实例,则返回 true,否则返回 false。 在这里需要特别注意的是:instanceof 检测的是原型。

用一段伪代码来模拟其内部执行过程:

instanceof (A,B) = {
    varL = A.__proto__;
    varR = B.prototype;
    if(L === R) {
        // A的内部属性 __proto__ 指向 B 的原型对象
        return true;
    }
    return false;
}

从上述过程可以看出,当 A 的 __proto__ 指向 Bprototype 时,就认为 A 就是 B 的实例。

需要注意的是,instanceof 只能用来判断两个对象是否属于实例关系, 而不能判断一个对象实例具体属于哪种类型。

例如:[ ] instanceof Object 返回的也会是 true

71. 引用类型有哪些,有什么特点

参考答案:

JS 中七种内置类型(null,undefined,boolean,number,string,symbol,object)又分为两大类型

两大类型:

  • 基本类型: nullundefinedbooleannumberstringsymbol
  • 引用类型Object: ArrayFunctionDateRegExp 基本类型和引用类型的主要区别有以下几点:

存放位置:

  • 基本数据类型:基本类型值在内存中占据固定大小,直接存储在栈内存中的数据
  • 引用数据类型:引用类型在栈中存储了指针,这个指针指向堆内存中的地址,真实的数据存放在堆内存里。 值的可变性:

  • 基本数据类型: 值不可变,javascript 中的原始值(undefined、null、布尔值、数字和字符串)是不可更改的

  • 引用数据类型:引用类型是可以直接改变其值的

比较:

  • 基本数据类型: 基本类型的比较是值的比较,只要它们的值相等就认为他们是相等的

  • 引用数据类型: 引用数据类型的比较是引用的比较,看其的引用是否指向同一个对象

72. 如何得到一个变量的类型---指函数封装实现

参考答案:

请参阅前面第 30 题答案。

73. 什么是作用域、闭包

参考答案:

请参阅前面第 56 题。

74. 闭包的缺点是什么?闭包的应用场景有哪些?怎么销毁闭包?

参考答案:

闭包是指有权访问另外一个函数作用域中的变量的函数。

因为闭包引用着另一个函数的变量,导致另一个函数已经不使用了也无法销毁,所以闭包使用过多,会占用较多的内存,这也是一个副作用,内存泄漏。

如果要销毁一个闭包,可以 把被引用的变量设置为null,即手动清除变量,这样下次 js 垃圾回收机制回收时,就会把设为 null 的量给回收了。

闭包的应用场景:

  1. 匿名自执行函数
  2. 结果缓存
  3. 封装
  4. 实现类和继承

75. JS的垃圾回收站机制

参考答案:

JS 具有自动垃圾回收机制。垃圾收集器会按照固定的时间间隔周期性的执行。

JS 常见的垃圾回收方式:标记清除、引用计数方式。

1、标记清除方式:

  • 工作原理:当变量进入环境时,将这个变量标记为“进入环境”。当变量离开环境时,则将其标记为“离开环境”。标记“离开环境”的就回收内存。

  • 工作流程:

  • 垃圾回收器,在运行的时候会给存储在内存中的所有变量都加上标记;

  • 去掉环境中的变量以及被环境中的变量引用的变量的标记;

  • 被加上标记的会被视为准备删除的变量;

  • 垃圾回收器完成内存清理工作,销毁那些带标记的值并回收他们所占用的内存空间。

2、引用计数方式:

  • 工作原理:跟踪记录每个值被引用的次数。

  • 工作流程:

  • 声明了一个变量并将一个引用类型的值赋值给这个变量,这个引用类型值的引用次数就是 1

  • 同一个值又被赋值给另一个变量,这个引用类型值的引用次数加1;

  • 当包含这个引用类型值的变量又被赋值成另一个值了,那么这个引用类型值的引用次数减 1

  • 当引用次数变成 0 时,说明没办法访问这个值了;

  • 当垃圾收集器下一次运行时,它就会释放引用次数是0的值所占的内存。

76. 什么是作用域链、原型链

参考答案:

什么是作用域链?

当访问一个变量时,编译器在执行这段代码时,会首先从当前的作用域中查找是否有这个标识符,如果没有找到,就会去父作用域查找,如果父作用域还没找到继续向上查找,直到全局作用域为止,,而作用域链,就是有当前作用域与上层作用域的一系列变量对象组成,它保证了当前执行的作用域对符合访问权限的变量和函数的有序访问。

什么原型链?

每个对象都可以有一个原型__proto__,这个原型还可以有它自己的原型,以此类推,形成一个原型链。查找特定属性的时候,我们先去这个对象里去找,如果没有的话就去它的原型对象里面去,如果还是没有的话再去向原型对象的原型对象里去寻找。这个操作被委托在整个原型链上,这个就是我们说的原型链。

77. new 一个构造函数发生了什么

参考答案:

new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。

new 关键字会进行如下的操作: 步骤 1:创建一个空的简单 JavaScript 对象,即 { } ; 步骤 2:链接该对象到另一个对象(即设置该对象的原型对象); 步骤 3:将步骤 1 新创建的对象作为 this 的上下文; 步骤 4:如果该函数没有返回对象,则返回 this

78. 对一个构造函数实例化后. 它的原型链指向什么

参考答案:

指向该构造函数实例化出来对象的原型对象。

对于构造函数来讲,可以通过 prototype 访问到该对象。

对于实例对象来讲,可以通过隐式属性 __proto__ 来访问到。

79. 什么是变量提升

参考答案:

JavaScript 编译所有代码时,所有使用 var 的变量声明都被提升到它们的函数/局部作用域的顶部(如果在函数内部声明的话),或者提升到它们的全局作用域的顶部(如果在函数外部声明的话),而不管实际的声明是在哪里进行的。这就是我们所说的“提升”。

请记住,这种“提升”实际上并不发生在你的代码中,而只是一种比喻,与 JavaScript 编译器如何读取你的代码有关。记住当我们想到“提升”的时候,我们可以想象任何被提升的东西都会被移动到顶部,但是实际上你的代码并不会被修改。

函数声明也会被提升,但是被提升到了最顶端,所以将位于所有变量声明之上。

在编译阶段变量和函数声明会被放入内存中,但是你在代码中编写它们的位置会保持不变。

80. == 和 === 的区别是什么

参考答案:

简单来说: == 代表相同, === 代表严格相同(数据类型和值都相等)。

当进行双等号比较时候,先检查两个操作数数据类型,如果相同,则进行===比较,如果不同,则愿意为你进行一次类型转换,转换成相同类型后再进行比较,而 === 比较时,如果类型不同,直接就是false。

从这个过程来看,大家也能发现,某些情况下我们使用 === 进行比较效率要高些,因此,没有歧义的情况下,不会影响结果的情况下,在 JS 中首选 === 进行逻辑比较。

81. Object.is 方法比较的是什么

参考答案:

Object.is 方法是 ES6 新增的用来比较两个值是否严格相等的方法,与 === (严格相等)的行为基本一致。不过有两处不同:

  • +0 不等于 -0。
  • NaN 等于自身。

所以可以将Object.is 方法看作是加强版的严格相等。

82. 基础数据类型和引用数据类型,哪个是保存在栈内存中?哪个是在堆内存中?

参考答案:

ECMAScript 规范中,共定义了 7 种数据类型,分为 基本类型引用类型 两大类,如下所示:

  • 基本类型String、Number、Boolean、Symbol、Undefined、Null

  • 引用类型Object

基本类型也称为简单类型,由于其占据空间固定,是简单的数据段,为了便于提升变量查询速度,将其存储在栈中,即按值访问。

引用类型也称为复杂类型,由于其值的大小会改变,所以不能将其存放在栈中,否则会降低变量查询速度,因此,其值存储在堆(heap)中,而存储在变量处的值,是一个指针,指向存储对象的内存处,即按址访问。引用类型除 Object 外,还包括 Function 、Array、RegExp、Date 等等。

83. 箭头函数解决了什么问题?

参考答案:

箭头函数主要解决了 this 的指向问题。

解析:

ES5 时代,一旦对象的方法里面又存在函数,则 this 的指向往往会让开发人员抓狂。

例如:

//错误案例,this 指向会指向 Windows 或者 undefined
var obj = {
 age: 18,
 getAge: function () {
     var a = this.age; // 18
     var fn = function () {
         return new Date().getFullYear() - this.age; // this 指向 window 或 undefined
     };
     return fn();
 }
};
console.log(obj.getAge()); // NaN

然而,箭头函数没有 this,箭头函数的 this 是继承父执行上下文里面的 this

var obj = {
 age: 18,
 getAge: function () {
     var a = this.age; // 18
     var fn = () => new Date().getFullYear() - this.age; // this 指向 obj 对象
     return fn();
 }
};

console.log(obj.getAge()); // 2003

84. new 一个箭头函数后,它的 this 指向什么?

参考答案:

我不知道这道题是出题人写错了还是故意为之。

箭头函数无法用来充当构造函数,所以是无法 new 一个箭头函数的。

当然,也有可能是面试官故意挖的一个坑,等着你往里面跳。

85. promise 的其他方法有用过吗?如 all、race。请说下这两者的区别

参考答案:

promise.all 方法参数是一个 promise 的数组,只有当所有的 promise 都完成并返回成功,才会调用 resolve,当有一个失败,都会进catch,被捕获错误,promise.all 调用成功返回的结果是每个 promise 单独调用成功之后返回的结果组成的数组,如果调用失败的话,返回的则是第一个 reject 的结果

promise.race 也会调用所有的 promise,返回的结果则是所有 promise 中最先返回的结果,不关心是成功还是失败。

86. class 是如何实现的

参考答案:

classES6 新推出的关键字,它是一个语法糖,本质上就是基于这个原型实现的。只不过在以前 ES5 原型实现的基础上,添加了一些 _classCallCheck、_defineProperties、_createClass等方法来做出了一些特殊的处理。

例如:

class Hello {
constructor(x) {
  this.x = x;
}
greet() {
  console.log("Hello, " + this.x)
}
}
"use strict";

function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
}
}

function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
    var descriptor = props[i];
    descriptor.enumerable = descriptor.enumerable || false;
    descriptor.configurable = true;
    if ("value" in descriptor)
        descriptor.writable = true;
    Object.defineProperty(target, descriptor.key, descriptor);
}
}

function _createClass(Constructor, protoProps, staticProps) {
console.log("Constructor::",Constructor);
console.log("protoProps::",protoProps);
console.log("staticProps::",staticProps);
if (protoProps)
    _defineProperties(Constructor.prototype, protoProps);
if (staticProps)
    _defineProperties(Constructor, staticProps);
return Constructor;
}

var Hello = /*#__PURE__*/function () {
function Hello(x) {
  _classCallCheck(this, Hello);

  this.x = x;
}

_createClass(Hello, [{
  key: "greet",
  value: function greet() {
    console.log("Hello, " + this.x);
  }
   }]);

   return Hello;
}();

87. let、const、var 的区别

参考答案:

请参阅前面第 22 题答案。

88. ES6 中模块化导入和导出与 common.js 有什么区别

参考答案:

CommonJs模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化不会影响到这个值.

// common.js
var count = 1;

var printCount = () =>{ 
return ++count;
}

module.exports = {
printCount: printCount,
count: count
};
// index.js
let v = require('./common');
console.log(v.count); // 1
console.log(v.printCount()); // 2
console.log(v.count); // 1

你可以看到明明common.js里面改变了count,但是输出的结果还是原来的。这是因为count是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动的值。将common.js里面的module.exports 改写成

module.exports = {
printCount: printCount,
get count(){
   return count
}
};

这样子的输出结果是 1,2,2

而在ES6当中,写法是这样的,是利用export 和import导入的

// es6.js
export let count = 1;
export function printCount() {
++count;
}
// main1.js
import  { count, printCount } from './es6';
console.log(count)
console.log(printCount());
console.log(count)

ES6 模块是动态引用,并且不会缓存,模块里面的变量绑定其所有的模块,而是动态地去加载值,并且不能重新赋值,

ES6 输入的模块变量,只是一个“符号连接符”,所以这个变量是只读的,对它进行重新赋值会报错。如果是引用类型,变量指向的地址是只读的,但是可以为其添加属性或成员。

另外还想说一个 export default

let count = 1;
function printCount() {
++count;
} 
export default { count, printCount}
// main3.js
import res form './main3.js'
console.log(res.count)

export与export default的区别及联系:

  1. export与export default均可用于导出常量、函数、文件、模块等

  2. 你可以在其它文件或模块中通过 import + (常量 | 函数 | 文件 | 模块)名的方式,将其导入,以便能够对其进行使用

  3. 在一个文件或模块中,export、import可以有多个,export default仅有一个

  4. 通过export方式导出,在导入时要加{ },export default则不需要。

89. 说一下普通函数和箭头函数的区别

参考答案:

请参阅前面第 8、25、83 题答案。

90. 说一下 promiseasyncawait 什么关系

参考答案:

await 表达式会造成异步函数停止执行并且等待promise的解决,当值被resolved,异步函数会恢复执行以及返回resolved值。如果该值不是一个promise,它将会被转换成一个resolved后的promise。如果promiserejectedawait 表达式会抛出异常值。

91. 说一下你学习过的有关 ES6 的知识点

参考答案:

这种题目是开放题,可以简单列举一下 ES6 的新增知识点。( ES6 的新增知识点参阅前面第 44 题)

然后说一下自己平时开发中用得比较多的是哪些即可。

一般面试官会针对你所说的内容进行二次提问。例如:你回答平时开发中箭头函数用得比较多,那么面试官极大可能针对箭头函数展开二次提问,询问你箭头函数有哪些特性?箭头函数 this 特点之类的问题。

92. 了解过 jsarguments 吗?接收的是实参还是形参?

参考答案:

JS 中的 arguments 是一个伪数组对象。这个伪数组对象将包含调用函数时传递的所有的实参。

与之相对的,JS 中的函数还有一个 length 属性,返回的是函数形参的个数。

93. ES6 相比于 ES5 有什么变化

参考答案:

ES6 相比 ES5 新增了很多新特性,这里可以自己简述几个。

具体的新增特性可以参阅前面第 44 题。

94. 强制类型转换方法有哪些?

参考答案:

JavaScript 中的数据类型转换,主要有三种方式:

  1. 转换函数

js 提供了诸如 parseIntparseFloat 这些转换函数,通过这些转换函数可以进行数据类型的转换 。

  1. 强制类型转换

还可使用强制类型转换(type casting)处理转换值的类型。

例如:

  • Boolean(value) 把给定的值转换成 Boolean 型;
  • Number(value)——把给定的值转换成数字(可以是整数或浮点数);
  • String(value)——把给定的值转换成字符串。

  • 利用 js 变量弱类型转换。

例如:

  • 转换字符串:直接和一个空字符串拼接,例如:a = "" + 数据

  • 转换布尔:!!数据类型,例如:!!"Hello"

  • 转换数值:数据1 或 /1,例如:`"Hello 1"`

95. 纯函数

参考答案:

一个函数,如果符合以下两个特点,那么它就可以称之为纯函数

  1. 对于相同的输入,永远得到相同的输出
  2. 没有任何可观察到的副作用

解析:

针对上面的两个特点,我们一个一个来看。

  • 相同输入得到相同输出

我们先来看一个不纯的反面典型:

let greeting = 'Hello'

function greet (name) {
  return greeting + ' ' + name
}

console.log(greet('World')) // Hello World

上面的代码中,greet('World') 是不是永远返回 Hello World ? 显然不是,假如我们修改 greeting 的值,就会影响 greet 函数的输出。即函数 greet 其实是 依赖外部状态 的。

那我们做以下修改:

function greet (greeting, name) {
  return greeting + ' ' + name
}

console.log(greet('Hi', 'Savo')) // Hi Savo

greeting 参数也传入,这样对于任何输入参数,都有与之对应的唯一的输出参数了,该函数就符合了第一个特点。

  • 没有副作用

副作用的意思是,这个函数的运行,不会修改外部的状态

下面再看反面典型:

const user = {
  username: 'savokiss'
}

let isValid = false

function validate (user) {
  if (user.username.length > 4) {
    isValid = true
  }
}

可见,执行函数的时候会修改到 isValid 的值(注意:如果你的函数没有任何返回值,那么它很可能就具有副作用!)

那么我们如何移除这个副作用呢?其实不需要修改外部的 isValid 变量,我们只需要在函数中将验证的结果 return 出来:

const user = {
  username: 'savokiss'
}

function validate (user) {
  return user.username.length > 4;
}

const isValid = validate(user)

这样 validate 函数就不会修改任何外部的状态了~

96. JS 模块化

参考答案:

模块化主要是用来抽离公共代码,隔离作用域,避免变量冲突等。

模块化的整个发展历史如下:

IIFE: 使用自执行函数来编写模块化,特点:在一个单独的函数作用域中执行代码,避免变量冲突

(function(){
return {
   data:[]
}
})()

AMD: 使用requireJS 来编写模块化,特点:依赖必须提前声明好

define('./index.js',function(code){
   // code 就是index.js 返回的内容
})

CMD: 使用seaJS 来编写模块化,特点:支持动态引入依赖文件

define(function(require, exports, module) {  
var indexCode = require('./index.js');
});

CommonJS: nodejs 中自带的模块化。

var fs = require('fs');

UMD:兼容AMD,CommonJS 模块化语法。

webpack(require.ensure):webpack 2.x 版本中的代码分割。

ES Modules: ES6 引入的模块化,支持import 来引入另一个 js 。

import a from 'a';

97. 看过 jquery 源码吗?

参考答案:

开放题,但是需要注意的是,如果看过 jquery 源码,不要简单的回答一个“看过”就完了,应该继续乘胜追击,告诉面试官例如哪个哪个部分是怎么怎么实现的,并针对这部分的源码实现,可以发表一些自己的看法和感想。

98. 说一下 js 中的 this

参考答案:

请参阅前面第 17 题答案。

99. apply call bind 区别,手写

参考答案:

apply call bind 区别 ?

callapply 的功能相同,区别在于传参的方式不一样:

  • fn.call(obj, arg1, arg2, ...) 调用一个函数, 具有一个指定的 this 值和分别地提供的参数(参数的列表)。
  • fn.apply(obj, [argsArray]) 调用一个函数,具有一个指定的 this 值,以及作为一个数组(或类数组对象)提供的参数。

bindcall/apply 有一个很重要的区别,一个函数被 call/apply 的时候,会直接调用,但是 bind 会创建一个新函数。当这个新函数被调用时,bind( ) 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。

实现 call 方法:

Function.prototype.call2 = function (context) {
  //没传参数或者为 null 是默认是 window
  var context = context || (typeof window !== 'undefined' ? window : global)
  // 首先要获取调用 call 的函数,用 this 可以获取
  context.fn = this
  var args = []
  for (var i = 1; i < arguments.length; i++) {
      args.push('arguments[' + i + ']')
  }
  eval('context.fn(' + args + ')')
  delete context.fn
}

// 测试
var value = 3
var foo = {
  value: 2
}

function bar(name, age) {
  console.log(this.value)
  console.log(name)
  console.log(age)
}
bar.call2(null)
// 浏览器环境: 3 undefinde undefinde   
// Node环境:undefinde undefinde undefinde

bar.call2(foo, 'cc', 18) // 2  cc 18

实现 apply 方法:

Function.prototype.apply2 = function (context, arr) {
  var context = context || (typeof window !== 'undefined' ? window : global)
  context.fn = this;

  var result;
  if (!arr) {
      result = context.fn();
  }
  else {
      var args = [];
      for (var i = 0, len = arr.length; i < len; i++) {
          args.push('arr[' + i + ']');
      }
      result = eval('context.fn(' + args + ')')
  }

  delete context.fn
  return result;
}

// 测试:

var value = 3
var foo = {
  value: 2
}

function bar(name, age) {
  console.log(this.value)
  console.log(name)
  console.log(age)
}
bar.apply2(null)
// 浏览器环境: 3 undefinde undefinde   
// Node环境:undefinde undefinde undefinde

bar.apply2(foo, ['cc', 18]) // 2  cc 18

实现 bind 方法:

Function.prototype.bind2 = function (oThis) {
  if (typeof this !== "function") {
      // closest thing possible to the ECMAScript 5 internal IsCallable function
      throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
  }
  var aArgs = Array.prototype.slice.call(arguments, 1),
      fToBind = this,
      fNOP = function () { },
      fBound = function () {
          return fToBind.apply(this instanceof fNOP && oThis
              ? this
              : oThis || window,
              aArgs.concat(Array.prototype.slice.call(arguments)));
      };

  fNOP.prototype = this.prototype;
  fBound.prototype = new fNOP();

  return fBound;
}

// 测试
var test = {
  name: "jack"
}
var demo = {
  name: "rose",
  getName: function () { return this.name; }
}

console.log(demo.getName()); // 输出 rose  这里的 this 指向 demo

// 运用 bind 方法更改 this 指向
var another2 = demo.getName.bind2(test);
console.log(another2()); // 输出 jack  这里 this 指向了 test 对象了

100. 手写 reduce flat

参考答案:

reduce 实现:

Array.prototype.my_reduce = function (callback, initialValue) {
 if (!Array.isArray(this) || !this.length || typeof callback !== 'function') {
     return []
 } else {
     // 判断是否有初始值
     let hasInitialValue = initialValue !== undefined;
     let value = hasInitialValue ? initialValue : tihs[0];
     for (let index = hasInitialValue ? 0 : 1; index < this.length; index++) {
         const element = this[index];
         value = callback(value, element, index, this)
     }
     return value
 }
}

let arr = [1, 2, 3, 4, 5]
let res = arr.my_reduce((pre, cur, i, arr) => {
 console.log(pre, cur, i, arr)
 return pre + cur
}, 10)
console.log(res)//25

flat 实现:

let arr = [1, [2, 3, [4, 5, [12, 3, "zs"], 7, [8, 9, [10, 11, [1, 2, [3, 4]]]]]]];

//万能的类型检测方法
const checkType = (arr) => {
 return Object.prototype.toString.call(arr).slice(8, -1);
}
//自定义flat方法,注意:不可以使用箭头函数,使用后内部的this会指向window
Array.prototype.myFlat = function (num) {
 //判断第一层数组的类型
 let type = checkType(this);
 //创建一个新数组,用于保存拆分后的数组
 let result = [];
 //若当前对象非数组则返回undefined
 if (!Object.is(type, "Array")) {
     return;
 }
 //遍历所有子元素并判断类型,若为数组则继续递归,若不为数组则直接加入新数组
 this.forEach((item) => {
     let cellType = checkType(item);
     if (Object.is(cellType, "Array")) {
         //形参num,表示当前需要拆分多少层数组,传入Infinity则将多维直接降为一维
         num--;
         if (num < 0) {
             let newArr = result.push(item);
             return newArr;
         }
         //使用三点运算符解构,递归函数返回的数组,并加入新数组
         result.push(...item.myFlat(num));
     } else {
         result.push(item);
     }
 })
 return result;
}
console.time();

console.log(arr.flat(Infinity)); //[1, 2, 3, 4, 5, 12, 3, "zs", 7, 8, 9, 10, 11, 1, 2, 3, 4];

console.log(arr.myFlat(Infinity)); //[1, 2, 3, 4, 5, 12, 3, "zs", 7, 8, 9, 10, 11, 1, 2, 3, 4];
//自定义方法和自带的flat返回结果一致!!!!
console.timeEnd();

本文链接:http://www.yanhongzhi.com/post/interview-javascript-2.html

-- EOF --

Comments