06月24, 2022

三、JavaScript 面试题汇总(101-150)

101. == 隐试转换的原理?是怎么转换的

参考答案:

两个与类型转换有关的函数:valueOf()和toString()

  • valueOf()的语义是,返回这个对象逻辑上对应的原始类型的值。比如说,String包装对象的valueOf(),应该返回这个对象所包装的字符串。
  • toString()的语义是,返回这个对象的字符串表示。用一个字符串来描述这个对象的内容。

valueOf()和toString()是定义在Object.prototype上的方法,也就是说,所有的对象都会继承到这两个方法。但是在Object.prototype上定义的这两个方法往往不能满足我们的需求(Object.prototype.valueOf()仅仅返回对象本身),因此js的许多内置对象都重写了这两个函数,以实现更适合自身的功能需要(比如说,String.prototype.valueOf就覆盖了在Object.prototype中定义的valueOf)。当我们自定义对象的时候,最好也重写这个方法。重写这个方法时要遵循上面所说的语义。

js内部用于实现类型转换的4个函数

这4个方法实际上是ECMAScript定义的4个抽象的操作,它们在js内部使用,进行类型转换。js的使用者不能直接调用这些函数。

  • ToPrimitive ( input [ , PreferredType ] )
  • ToBoolean ( argument )
  • ToNumber ( argument )
  • ToString ( argument )

需要区分这里的 ToString() 和上文谈到的 toString(),一个是 js 引擎内部使用的函数,另一个是定义在对象上的函数。

(1)ToPrimitive ( input [ , PreferredType ] )

将 input 转化成一个原始类型的值。PreferredType参数要么不传入,要么是Number 或 String。如果PreferredType参数是Number,ToPrimitive这样执行:

  1. 如果input本身就是原始类型,直接返回input。
  2. 调用input.valueOf(),如果结果是原始类型,则返回这个结果。
  3. 调用input.toString(),如果结果是原始类型,则返回这个结果。
  4. 抛出TypeError异常。

以下是PreferredType不为Number时的执行顺序。

  • 如果PreferredType参数是String,则交换上面这个过程的第2和第3步的顺序,其他执行过程相同。
  • 如果PreferredType参数没有传入
    • 如果input是内置的Date类型,PreferredType 视为String
    • 否则PreferredType 视为 Number

可以看出,ToPrimitive依赖于valueOf和toString的实现。

(2)ToBoolean ( argument )

image-20210819164742154

只需要记忆 0, null, undefined, NaN, "" 返回 false 就可以了,其他一律返回 true

(3)ToNumber ( argument )

image-20210819164927980

ToNumber的转化并不总是成功,有时会转化成NaN,有时则直接抛出异常。

(4)ToString ( argument )

image-20210819165004906

当js期望得到某种类型的值,而实际在那里的值是其他的类型,就会发生隐式类型转换。系统内部会自动调用我们前面说ToBoolean ( argument )、ToNumber ( argument )、ToString ( argument ),尝试转换成期望的数据类型。

102. ['1', '2', '3'].map(parseInt) 结果是什么,为什么 (字节)

参考答案:

[1, NaN, NaN]

解析:

一、为什么会是这个结果?

  1. map 函数

将数组的每个元素传递给指定的函数处理,并返回处理后的数组,所以 ['1','2','3'].map(parseInt) 就是将字符串 1,2,3 作为元素;0,1,2 作为下标分别调用 parseInt 函数。即分别求出 parseInt('1',0), parseInt('2',1), parseInt('3',2) 的结果。

  1. parseInt 函数(重点)

概念:以第二个参数为基数来解析第一个参数字符串,通常用来做十进制的向上取整(省略小数)如:parseInt(2.7) //结果为2

特点:接收两个参数 parseInt(string,radix)

string:字母(大小写均可)、数组、特殊字符(不可放在开头,特殊字符及特殊字符后面的内容不做解析)的任意字符串,如 '2'、'2w'、'2!'

radix:解析字符串的基数,基数规则如下:

1) 区间范围介于 2~36 之间;

2 ) 当参数为 0parseInt( ) 会根据十进制来解析;

3 ) 如果忽略该参数,默认的基数规则:

​ 如果 string 以 "0x" 开头,parseInt() 会把 string 的其余部分解析为十六进制的整数;parseInt("0xf") // 15 ​ 如果 string 以 0 开头,其后的字符解析为八进制或十六进制的数字;parseInt("08") // 8 ​ 如果 string 以 1 ~ 9 的数字开头,parseInt() 将把它解析为十进制的整数;parseInt("88.99f") // 88 ​ 只有字符串中的第一个数字会被返回。parseInt("10.33") // 返回10; ​ 开头和结尾的空格是允许的。parseInt(" 69 10 ") // 返回69 ​ 如果字符串的第一个字符不能被转换为数字,返回 NaN。parseInt("f") // 返回 NaN 而 parseInt("f",16) // 返回15

二、parseInt 方法解析的运算过程

parseInt('101.55',10); // 以十进制解析,运算过程:向上取整数(不做四舍五入,省略小数),结果为 101。

parseInt('101',2); // 以二进制解析,运算过程:12的2次方+02的1次方+1*2的0次方=4+0+1=5,结果为 5。

parseInt('101',8); // 以八进制解析,运算过程:18的2次方+08的1次方+1*8的0次方=64+0+1=65,结果为 65。

parseInt('101',16); // 以十六进制解析,运算过程:116的2次方+016的1次方+1*16的0次方=256+0+1=257,结果为 257。

三、再来分析一下结果

['1','2','3'].map(parseInt)

parseInt('1',0); radix 为 0,parseInt( ) 会根据十进制来解析,所以结果为 1

parseInt('2',1); radix 为 1,超出区间范围,所以结果为 NaN

parseInt('3',2); radix 为 2,用2进制来解析,应以 01 开头,所以结果为 NaN

103. 防抖,节流是什么,如何实现 (字节)

参考答案:

我们在平时开发的时候,会有很多场景会频繁触发事件,比如说搜索框实时发请求,onmousemove、resize、onscroll 等,有些时候,我们并不能或者不想频繁触发事件,这时候就应该用到函数防抖和函数节流。

函数防抖(debounce),指的是短时间内多次触发同一事件,只执行最后一次,或者只执行最开始的一次,中间的不执行。

具体实现:

/**
 * 函数防抖
 * @param {function} func 一段时间后,要调用的函数
 * @param {number} wait 等待的时间,单位毫秒
 */
function debounce(func, wait){
    // 设置变量,记录 setTimeout 得到的 id
    let timerId = null;
    return function(...args){
        if(timerId){
            // 如果有值,说明目前正在等待中,清除它
            clearTimeout(timerId);
        }
        // 重新开始计时
        timerId = setTimeout(() => {
            func(...args);
        }, wait);
    }
}

函数节流(throttle),指连续触发事件但是在 n 秒中只执行一次函数。即 2n 秒内执行 2 次... 。节流如字面意思,会稀释函数的执行频率。

具体实现:

function throttle(func, wait) {
    let context, args;
    let previous = 0;
    return function () {
        let now = +new Date();
        context = this;
        args = arguments;
        if (now - previous > wait) {
            func.apply(context, args);
            previous = now;
        }
    }
}

104. 介绍下 Set、Map、WeakSetWeakMap 的区别(字节)

参考答案:

Set

  • 成员唯一、无序且不重复

  • 键值与键名是一致的(或者说只有键值,没有键名)

  • 可以遍历,方法有 add, delete,has

WeakSet

  • 成员都是对象

  • 成员都是弱引用,可以被垃圾回收机制回收,可以用来保存 DOM 节点,不容易造成内存泄漏

  • 不能遍历,方法有 add, delete,has

Map

  • 本质上是健值对的集合,类似集合

  • 可以遍历,方法很多,可以跟各种数据格式转换

WeakMap

  • 只接受对象作为健名(null 除外),不接受其他类型的值作为健名
  • 键名是弱引用,键值可以是任意的,键名所指向的对象可以被垃圾机制回收,此时键名是无效的

  • 不能遍历,方法有 get、set、has、delete

105. setTimeout、Promise、Async/Await 的区别(字节)

参考答案:

事件循环中分为宏任务队列和微任务队列。

其中 setTimeout 的回调函数放到宏任务队列里,等到执行栈清空以后执行;

promise.then 里的回调函数会放到相应宏任务的微任务队列里,等宏任务里面的同步代码执行完再执行;

async 函数表示函数里面可能会有异步方法,await 后面跟一个表达式,async 方法执行时,遇到 await 会立即执行表达式,然后把表达式后面的代码放到微任务队列里,让出执行栈让同步代码先执行。

106. Promise 构造函数是同步执行还是异步执行,那么 then 方法呢?(字节)

参考答案:

promise 构造函数是同步执行的,then 方法是异步执行,then 方法中的内容加入微任务中。

107. 情人节福利题,如何实现一个 new (字节)

参考答案: 首先我们需要明白 new 的原理。关于 new 的原理,主要分为以下几步:

  • 创建一个空对象 。

  • this 变量引用该对象 。

  • 该对象继承该函数的原型(更改原型链的指向) 。

  • 把属性和方法加入到 this 引用的对象中。

  • 新创建的对象由 this 引用 ,最后隐式地返回 this

明白了这个原理后,我们就可以尝试来实现一个 new 方法,参考示例如下:

// 构造器函数
let Parent = function (name, age) {
 this.name = name;
 this.age = age;
};
Parent.prototype.sayName = function () {
 console.log(this.name);
};
//自己定义的new方法
let newMethod = function (Parent, ...rest) {
 // 1.以构造器的prototype属性为原型,创建新对象;
 let child = Object.create(Parent.prototype);
 // 2.将this和调用参数传给构造器执行
 let result = Parent.apply(child, rest);
 // 3.如果构造器没有手动返回对象,则返回第一步的对象
 return typeof result === 'object' ? result : child;
};
//创建实例,将构造函数Parent与形参作为参数传入
const child = newMethod(Parent, 'echo', 26);
child.sayName() //'echo';
//最后检验,与使用new的效果相同
console.log(child instanceof Parent)//true
console.log(child.hasOwnProperty('name'))//true
console.log(child.hasOwnProperty('age'))//true
console.log(child.hasOwnProperty('sayName'))//false

108. 实现一个 sleep 函数(字节)

参考答案:

function sleep(delay) {
 var start = (new Date()).getTime();
 while ((new Date()).getTime() - start < delay) {
     continue;
 }
}

function test() {
 console.log('111');
 sleep(2000);
 console.log('222');
}

test()

这种实现方式是利用一个伪死循环阻塞主线程。因为 JS 是单线程的。所以通过这种方式可以实现真正意义上的 sleep

109. 使用 sort() 对数组 [3, 15, 8, 29, 102, 22] 进行排序,输出结果 (字节)

参考答案:

sort 方法默认按照 ASCII 码来排序,如果要按照数字大小来排序,需要传入一个回调函数,如下:

[3, 15, 8, 29, 102, 22].sort((a,b) => {return a - b});

110. 实现 5.add(3).sub(2) (百度)

参考答案:

这里想要实现的是链式操作,那么我们可以考虑在 Number 类型的原型上添加 addsub 方法,这两个方法返回新的数

示例如下:

Number.prototype.add = function (number) {
 if (typeof number !== 'number') {
     throw new Error('请输入数字~');
 }
 return this.valueOf() + number;
};
Number.prototype.minus = function (number) {
 if (typeof number !== 'number') {
     throw new Error('请输入数字~');
 }
 return this.valueOf() - number;
};
console.log((5).add(3).minus(2)); // 6

111. 给定两个数组,求交集

参考答案:

示例代码如下:

function intersect(nums1, nums2) {
 let i = j = 0,
     len1 = nums1.length,
     len2 = nums2.length,
     newArr = [];
 if (len1 === 0 || len2 === 0) {
     return newArr;
 }
 nums1.sort(function (a, b) {
     return a - b;
 });
 nums2.sort(function (a, b) {
     return a - b;
 });
 while (i < len1 || j < len2) {
     if (nums1[i] > nums2[j]) {
         j++;
     } else if (nums1[i] < nums2[j]) {
         i++;
     } else {
         if (nums1[i] === nums2[j]) {
             newArr.push(nums1[i]);
         }
         if (i < len1 - 1) {
             i++;
         } else {
             break;
         }
         if (j < len2 - 1) {
             j++;
         } else {
             break;
         }
     }
 }
 return newArr;
};
// 测试
console.log(intersect([3, 5, 8, 1], [2, 3]));

112. 为什么普通 for 循环的性能远远高于 forEach 的性能,请解释其中的原因。

参考答案:

for 循环按顺序遍历,forEach 使用 iterator 迭代器遍历

下面是一段性能测试的代码:

let arrs = new Array(100000);
console.time('for');
for (let i = 0; i < arrs.length; i++) {
};
console.timeEnd('for');
console.time('forEach');
arrs.forEach((arr) => {
});
console.timeEnd('forEach');

for: 2.263ms
forEach: 0.254ms

在10万这个级别下,forEach的性能是for的十倍

for: 2.263ms
forEach: 0.254ms

在100万这个量级下,forEach的性能是和for的一致

for: 2.844ms
forEach: 2.652ms

在1000万级以上的量级上 ,forEach的性能远远低于for的性能

for: 8.422ms
forEach: 30.328m

我们从语法上面来观察:

arr.forEach(callback(currentValue [, index [, array]])[, thisArg])

可以看到 forEach 是有回调的,它会按升序为数组中含有效值的每一项执行一次 callback,且除了抛出异常以外,也没有办法中止或者跳出 forEach 循环。那这样的话执行就会额外的调用栈和函数内的上下文。

for 循环则是底层写法,不会产生额外的消耗。

在实际业务中没有很大的数组时,forforEach 的性能差距其实很小,forEach 甚至会优于 for 的时间,且更加简洁,可读性也更高,一般也会优先使用 forEach 方法来进行数组的循环处理。

113. 实现一个字符串匹配算法,从长度为 n 的字符串 S 中,查找是否存在字符串 T,T 的长度是 m,若存在返回所在位置。

参考答案:

// 完全不用 API
var getIndexOf = function (s, t) {
 let n = s.length;
 let m = t.length;
 if (!n || !m || n < m) return -1;
 for (let i = 0; i < n; i++) {
     let j = 0;
     let k = i;
     if (s[k] === t[j]) {
         k++; j++;
         while (k < n && j < m) {
             if (s[k] !== t[j]) break;
             else {
                 k++; j++;
             }
         }
         if (j === m) return i;
     }
 }
 return -1;
}

// 测试
console.log(getIndexOf("Hello World", "rl"))

114. 使用 JavaScript Proxy 实现简单的数据绑定

参考答案:

示例代码如下:

<body>
hello,world
<input type="text" id="model">
<p id="word"></p>
</body>
<script>
const model = document.getElementById("model")
const word = document.getElementById("word")
var obj= {};

const newObj = new Proxy(obj, {
   get: function(target, key, receiver) {
     console.log(`getting ${key}!`);
     return Reflect.get(target, key, receiver);
   },
   set: function(target, key, value, receiver) {
     console.log('setting',target, key, value, receiver);
     if (key === "text") {
       model.value = value;
       word.innerHTML = value;
     }
     return Reflect.set(target, key, value, receiver);
   }
 });

model.addEventListener("keyup",function(e){
 newObj.text = e.target.value
})
</script>

115. 数组里面有 10 万个数据,取第一个元素和第 10 万个元素的时间相差多少(字节)

参考答案:

消耗时间几乎一致,差异可以忽略不计

解析:

  • 数组可以直接根据索引取的对应的元素,所以不管取哪个位置的元素的时间复杂度都是 O(1)
  • JavaScript 没有真正意义上的数组,所有的数组其实是对象,其“索引”看起来是数字,其实会被转换成字符串,作为属性名(对象的 key)来使用。所以无论是取第 1 个还是取第 10 万个元素,都是用 key 精确查找哈希表的过程,其消耗时间大致相同。

116. 打印出 1~10000 以内的对称数

参考答案:

function isSymmetryNum(start, end) {
 for (var i = start; i < end + 1; i++) {
     var iInversionNumber = +(i.toString().split("").reverse().join(""));

     if (iInversionNumber === i && i > 10) {
         console.log(i);
     }

 }
}
isSymmetryNum(1, 10000);

117. 简述同步和异步的区别

参考答案:

同步意味着每一个操作必须等待前一个操作完成后才能执行。 异步意味着操作不需要等待其他操作完成后才开始执行。 在 JavaScript 中,由于单线程的特性导致所有代码都是同步的。但是,有些异步操作(例如:XMLHttpRequestsetTimeout)并不是由主线程进行处理的,他们由本机代码(浏览器 API)所控制,并不属于程序的一部分。但程序中被执行的回调部分依旧是同步的。

加分回答:

  • JavaScript 中的同步任务是指在主线程上排队执行的任务,只有前一个任务执行完成后才能执行后一个任务;异步任务是指进入任务队列(task queue)而非主线程的任务,只有当任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程中进行执行。
  • JavaScript 的并发模型是基于 “event loop”。
  • alert 这样的方法回阻塞主线程,以致用户关闭他后才能继续进行后续的操作。
  • JavaScript 主要用于和用户互动及操作 DOM,多线程的情况和异步操作带来的复杂性相比决定了他单线程的特性。
  • Web Worker 虽然允许 JavaScript 创建多个线程,但子线程完全受主线程控制,且不能操作 DOM。因此他还是保持了单线程的特性。

118. 怎么添加、移除、复制、创建、和查找节点

参考答案:

1)创建新节点

createDocumentFragment( ) // 创建一个DOM 片段

createElement( ) // 创建一个具体的元素

createTextNode( ) // 创建一个文本节点

(2)添加、移除、替换、插入

appendChild( )

removeChild( )

replaceChild( )

insertBefore( ) // 在已有的子节点前插入一个新的子节点

(3)查找

getElementsByTagName( ) //通过标签名称

getElementsByName( ) // 通过元素的 Name 属性的值

getElementById( ) // 通过元素 Id,唯一性

querySelector( ) // 用于接收一个 CSS 选择符,返回与该模式匹配的第一个元素

querySelectorAll( ) // 用于选择匹配到的所有元素

119. 实现一个函数 clone 可以对 Javascript 中的五种主要数据类型(Number、string、 Object、Array、Boolean)进行复制

参考答案:

示例代码如下:

/**
* 对象克隆
* 支持基本数据类型及对象
* 递归方法
*/
function clone(obj) {
    var o;
    switch (typeof obj) {
        case "undefined":
            break;
        case "string":
            o = obj + "";
            break;
        case "number":
            o = obj - 0;
            break;
        case "boolean":
            o = obj;
            break;
        case "object": // object 分为两种情况 对象(Object)或数组(Array)
            if (obj === null) {
                o = null;
            } else {
                if (Object.prototype.toString.call(obj).slice(8, -1) === "Array") {
                    o = [];
                    for (var i = 0; i < obj.length; i++) {
                        o.push(clone(obj[i]));
                    }
                } else {
                    o = {};
                    for (var k in obj) {
                        o[k] = clone(obj[k]);
                    }
                }
            }
            break;
        default:
            o = obj;
            break;
    }
    return o;
}

120. 如何消除一个数组里面重复的元素

参考答案:

请参阅前面第 2 题。

121. 写一个返回闭包的函数

参考答案:

function foo() {
 var i = 0;
 return function () {
     console.log(i++);
 }
}
var f1 = foo();
f1(); // 0
f1(); // 1
f1(); // 2

122. 使用递归完成 1 到 100 的累加

参考答案:

function add(x, y){
 if(x === y){
     return x;
 } else {
     return y + add(x, y-1);
 }
}

console.log(add(1, 100))

123. Javascript 有哪几种数据类型

参考答案:

请参阅前面第 26 题。

124. 如何判断数据类型

参考答案:

请参阅前面第 69 题。

125. console.log(1+'2')和 console.log(1-'2')的打印结果

参考答案:

第一个打印出 '12',是一个 string 类型的值。

第二个打印出 -1,是一个 number 类型的值

126. JS 的事件委托是什么,原理是什么

参考答案:

事件委托,又被称之为事件代理。在 JavaScript 中,添加到页面上的事件处理程序数量将直接关系到页面整体的运行性能。导致这一问题的原因是多方面的。

首先,每个函数都是对象,都会占用内存。内存中的对象越多,性能就越差。其次,必须事先指定所有事件处理程序而导致的 DOM 访问次数,会延迟整个页面的交互就绪时间。

对事件处理程序过多问题的解决方案就是事件委托。

事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。例如,click 事件会一直冒泡到 document 层次。也就是说,我们可以为整个页面指定一个 onclick 事件处理程序,而不必给每个可单击的元素分别添加事件处理程序。

127. 如何改变函数内部的 this 指针的指向

参考答案:

可以通过 call、apply、bind 方法来改变 this 的指向,关于 call、apply、bind 方法的具体使用,请参阅前面 102

128. JS 延迟加载的方式有哪些?

参考答案:

  • defer 属性
  • async 属性
  • 使用 jQuerygetScript( ) 方法
  • 使用 setTimeout 延迟方法
  • JS 外部引入的文件放到页面底部,来让 JS 最后引入

129. 说说严格模式的限制

参考答案:

什么是严格模式?

严格模式对 JavaScript 的语法和行为都做了一些更改,消除了语言中一些不合理、不确定、不安全之处;提供高效严谨的差错机制,保证代码安全运行;禁用在未来版本中可能使用的语法,为新版本做好铺垫。在脚本文件第一行或函数内第一行中引入"use strict"这条指令,就能触发严格模式,这是一条没有副作用的指令,老版的浏览器会将其作为一行字符串直接忽略。

例如:

"use strict";//脚本第一行
function add(a,b){
    "use strict";//函数内第一行
    return a+b;
}

进入严格模式后的限制

  • 变量必须声明后再赋值
  • 不能有重复的参数名,函数的参数也不能有同名属性
  • 不能使用with语句
  • 不能对只读属性赋值
  • 不能使用前缀 0表示八进制数
  • 不能删除不可删除的属性
  • eval 不会在它的外层作用域引入变量。
  • evalarguments不能被重新赋值
  • arguments 不会自动反应函数的变化
  • 不能使用 arguments.callee
  • 不能使用 arguments.caller
  • 禁止 this 指向全局对象
  • 不能使用 fn.callerfn.arguments 获取函数调用的堆栈
  • 增加了保留字

130. attributeproperty 的区别是什么?

参考答案:

property 和 attribute 非常容易混淆,两个单词的中文翻译也都非常相近(property:属性,attribute:特性),但实际上,二者是不同的东西,属于不同的范畴。

  • property是DOM中的属性,是JavaScript里的对象;
  • attribute是HTML标签上的特性,它的值只能够是字符串;

简单理解,Attribute就是dom节点自带的属性,例如html中常用的id、class、title、align等。

而Property是这个DOM元素作为对象,其附加的内容,例如childNodes、firstChild等。

131. ES6 能写 class 么,为什么会出现 class 这种东西?

参考答案:

ES6 中,可以书写 class。因为在 ES6 规范中,引入了 class 的概念。使得 JS 开发者终于告别了直接使用原型对象模仿面向对象中的类和类继承时代。

但是 JS 中并没有一个真正的 class 原始类型, class 仅仅只是对原型对象运用语法糖。

之所以出现 class 关键字,是为了使 JS 更像面向对象,所以 ES6 才引入 class 的概念。

132. 常见兼容性问题

参考答案:

常见的兼容性问题很多,这里列举一些:

  1. 关于获取行外样式 currentStylegetComputedStyle 出现的兼容问题

我们都知道 JS 通过 style 不可以获取行外样式,如果我们需要获取行外样式就会使用这两种

  • IE 下:currentStyle

  • chrome、FF 下:getComputedStyle 第二个参数的作用是获取伪类元素的属性值

  1. 关于“索引”获取字符串每一项出现的兼容性的问题

对于字符串也有类似于数组这样通过下标索引获取每一项的值

var str = 'abcd';
console.log(str[2]);

但是低版本的浏览器 IE6、7 不兼容

  1. 关于使用 firstChild、lastChild 等,获取第一个/最后一个元素节点是产生的问题

  2. IE6-8下: firstChild,lastChild,nextSibling,previousSibling 获取第一个元素节点

  3. 高版本浏览器IE9+、FF、Chrome:获取的空白文本节点
  1. 关于使用 event 对象,出现兼容性问题

IE8 及之前的版本浏览器中,event 事件对象是作为 window 对象的一个属性。

所以兼容的写法如下:

function(event){
    event = event || window.event;
}
  1. 关于事件绑定的兼容性问题

  2. IE8 以下用: attachEvent('事件名',fn);

  3. FF、Chrome、IE9-10 用: attachEventLister('事件名',fn,false);

  1. 关于获取滚动条距离而出现的问题

当我们获取滚动条滚动距离时:

  • IE、Chrome: document.body.scrollTop

  • FF: document.documentElement.scrollTop

兼容处理:

var scrollTop = document.documentElement.scrollTop||document.body.scrollTop

133. 函数防抖节流的原理

参考答案:

请参阅前面第 49、106 题。

134. 原始类型有哪几种?null 是对象吗?

参考答案:

JavaScript 中,数据类型整体上来讲可以分为两大类:基本类型引用数据类型

基本数据类型,一共有 7 种:

string,symbol,number,boolean,undefined,null,bigInt

其中 symbol 类型是在 ES6 里面新添加的基本数据类型。

引用数据类型,就只有 1 种:

object

基本数据类型的值又被称之为原始值或简单值,而引用数据类型的值又被称之为复杂值或引用值。

关于原始类型和引用类型的区别,可以参阅第 26 题。

null 表示空,但是当我们使用 typeof 来进行数据类型检测的时候,得到的值是 object

具体原因可以参阅前面第 68 题。

135. 为什么 console.log(0.2+0.1==0.3) // false

参考答案:

因为浮点数的计算存在 round-off 问题,也就是浮点数不能够进行精确的计算。并且:

  • 不仅 JavaScript,所有遵循 IEEE 754 规范的语言都是如此;
  • JavaScript 中,所有的 Number 都是以 64-bit 的双精度浮点数存储的;
  • 双精度的浮点数在这 64 位上划分为 3 段,而这 3 段也就确定了一个浮点数的值,64bit 的划分是“1-11-52”的模式,具体来说:
    • 就是 1 位最高位(最左边那一位)表示符号位,0 表示正,1 表示负;
    • 11 位表示指数部分;
    • 52 位表示尾数部分,也就是有效域部分

136. 说一下 JS 中类型转换的规则?

参考答案:

类型转换可以分为两种,隐性转换显性转换

1. 隐性转换

当不同数据类型之间进行相互运算,或者当对非布尔类型的数据求布尔值的时候,会发生隐性转换。

预期为数字的时候:算术运算的时候,我们的结果和运算的数都是数字,数据会转换为数字来进行计算。

类型 转换前 转换后
number 4 4
string "1" 1
string "abc" NaN
string "" 0
boolean true 1
boolean false 0
undefined undefined NaN
null null 0

预期为字符串的时候:如果有一个操作数为字符串时,使用+符号做相加运算时,会自动转换为字符串。

预期为布尔的时候:前面在介绍布尔类型时所提到的 9 个值会转为 false,其余转为 true

2. 显性转换

所谓显性转换,就是只程序员强制将一种类型转换为另外一种类型。显性转换往往会使用到一些转换方法。常见的转换方法如下:

  • 转换为数值类型:Number()parseInt()parseFloat()

  • 转换为布尔类型:Boolean()

  • 转换为字符串类型:toString()String()

当然,除了使用上面的转换方法,我们也可以通过一些快捷方式来进行数据类型的显性转换,如下:

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

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

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

137. 深拷贝和浅拷贝的区别?如何实现

参考答案:

  • 浅拷贝:只是拷贝了基本类型的数据,而引用类型数据,复制后也是会发生引用,我们把这种拷贝叫做浅拷贝(浅复制)

    浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。

  • 深拷贝:在堆中重新分配内存,并且把源对象所有属性都进行新建拷贝,以保证深拷贝的对象的引用图不包含任何原有对象或对象图上的任何对象,拷贝后的对象与原来的对象是完全隔离,互不影响。

浅拷贝方法

  1. 直接赋值
  2. Object.assign 方法:可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。当拷贝的 object 只有一层的时候,是深拷贝,但是当拷贝的对象属性值又是一个引用时,换句话说有多层时,就是一个浅拷贝。
  3. ES6 扩展运算符,当 object 只有一层的时候,也是深拷贝。有多层时是浅拷贝。
  4. Array.prototype.concat 方法
  5. Array.prototype.slice 方法
  6. jQuery 中的 $.extend:在 jQuery 中,$.extend(deep,target,object1,objectN) 方法可以进行深浅拷贝。deep 如过设为 true 为深拷贝,默认是 false 浅拷贝。

深拷贝方法

  1. $.extend(deep,target,object1,objectN),将 deep 设置为 true
  2. JSON.parse(JSON.stringify):用 JSON.stringify 将对象转成 JSON 字符串,再用 JSON.parse 方法把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。这种方法虽然可以实现数组或对象深拷贝,但不能处理函数。
  3. 手写递归

示例代码如下:

function deepCopy(oldObj, newobj) {
    for (var key in oldObj) {
        var item = oldObj[key];
        // 判断是否是对象
        if (item instanceof Object) {
            if (item instanceof Function) {
                newobj[key] = oldObj[key];
            } else {
                newobj[key] = {};  //定义一个空的对象来接收拷贝的内容
                deepCopy(item, newobj[key]); //递归调用
            }

            // 判断是否是数组
        } else if (item instanceof Array) {
            newobj[key] = [];  //定义一个空的数组来接收拷贝的内容
            deepCopy(item, newobj[key]); //递归调用
        } else {
            newobj[key] = oldObj[key];
        }
    }
}

138. 如何判断 this?箭头函数的 this 是什么

参考答案:

有关如何判断 this,可以参阅前面 17 题。

有关箭头函数的 this 指向,可以参阅前面 24、25

139. call、apply 以及 bind 函数内部实现是怎么样的

参考答案:

请参阅前面 102 题。

140. 为什么会出现 setTimeout 倒计时误差?如何减少

参考答案:

定时器是属于宏任务(macrotask) 。如果当前执行栈所花费的时间大于定时器时间,那么定时器的回调在宏任务(macrotask) 里,来不及去调用,所有这个时间会有误差。

141. 谈谈你对 JS 执行上下文栈和作用域链的理解

参考答案:

什么是执行上下文?

简而言之,执行上下文是评估和执行 JavaScript 代码的环境的抽象概念。每当 Javascript 代码在运行的时候,它都是在执行上下文中运行。

执行上下文的类型

JavaScript 中有三种执行上下文类型。

  • 全局执行上下文 — 这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。一个程序中只会有一个全局执行上下文。
  • 函数执行上下文 — 每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序(将在后文讨论)执行一系列步骤。
  • Eval 函数执行上下文 — 执行在 eval 函数内部的代码也会有它属于自己的执行上下文。

调用栈

调用栈是解析器(如浏览器中的的javascript解析器)的一种机制,可以在脚本调用多个函数时,跟踪每个函数在完成执行时应该返回控制的点。(如什么函数正在执行,什么函数被这个函数调用,下一个调用的函数是谁)

  • 当脚本要调用一个函数时,解析器把该函数添加到栈中并且执行这个函数。
  • 任何被这个函数调用的函数会进一步添加到调用栈中,并且运行到它们被上个程序调用的位置。
  • 当函数运行结束后,解释器将它从堆栈中取出,并在主代码列表中继续执行代码。
  • 如果栈占用的空间比分配给它的空间还大,那么则会导致“栈溢出”错误。

作用域链

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

142. new 的原理是什么?通过 new 的方式创建对象和通过字面量创建有什么区别?

参考答案:

关于 new 的原理,主要分为以下几步:

  • 创建一个空对象 。

  • this 变量引用该对象 。

  • 该对象继承该函数的原型(更改原型链的指向) 。

  • 把属性和方法加入到 this 引用的对象中。

  • 新创建的对象由 this 引用 ,最后隐式地返回 this,过程如下:

var obj = {};
obj.__proto__ = Base.prototype;
Base.call(obj);

通过 new 的方式创建对象和通过字面量创建的对象,区别在于 new 出来的对象的原型对象为构造函数.prototype,而字面量对象的原型对象为 Object.prototype

示例代码如下:

function Computer() {}
var c = new Computer();
var d = {};
console.log(c.__proto__ === Computer.prototype); // true
console.log(d.__proto__ === Object.prototype); // true

143. prototype 和 __proto__ 区别是什么?

参考答案:

prototype 是构造函数上面的一个属性,指向实例化出来对象的原型对象。

__proto__ 是对象上面的一个隐式属性,指向自己的原型对象。

144. 使用 ES5 实现一个继承?

参考答案:

请参阅第 47 题。

145. 取数组的最大值(ES5、ES6

参考答案:

var arr = [3, 5, 8, 1];
// ES5 方式
console.log(Math.max.apply(null, arr)); // 8
// ES6 方式
console.log(Math.max(...arr)); // 8

146. ES6 新的特性有哪些?

参考答案:

请参阅前面第 44 题。

147. Promise 有几种状态, Promise 有什么优缺点 ?

参考答案:

Promise 有三种状态:

pending、fulfilled、rejected(未决定,履行,拒绝),同一时间只能存在一种状态,且状态一旦改变就不能再变。Promise 是一个构造函数,promise 对象代表一项有两种可能结果(成功或失败)的任务,它还持有多个回调,出现不同结果时分别发出相应回调。

  • 初始化状态:pending
  • 当调用 resolve(成功) 状态:pengding=>fulfilled
  • 当调用 reject(失败) 状态:pending=>rejected

Promise 的优点是解决了回调地狱,缺点是代码并没有因为新方法的出现而减少,反而变得更加复杂,同时理解难度也加大。所以后面出现了 async/await 的异步解决方案。

148. Promise 构造函数是同步还是异步执行,then 呢 ? Promise 如何实现 then 处理 ?

参考答案:

promise 构造函数是同步执行的,then 方法是异步执行,then 方法中的内容加入微任务中。

接下来我们来看 promise 如何实现 then 的处理。

我们知道 then 是用来处理 resolvereject 函数的回调。那么首先我们来定义 then 方法。

1、then方法需要两个参数,其中onFulfilled代表resolve成功的回调,onRejected代表reject失败的回调。
then(onFulfilled,onRejected){}
2、我们知道promise的状态是不可逆的,在状态发生改变后,即不可再次更改,只有状态为FULFILLED才会调用onFulfilled,状态为REJECTED调用onRejected
then(onFulfilled, onRejected){
 if (this.status == Promise.FULFILLED) {
     onFulfilled(this.value)
 }
 if (this.status == Promise.REJECTED) {
     onRejected(this.value)
 }
}
3、then方法的每个方法都不是必须的,所以我们要处理当没有传递参数时,应该设置默认值
then(onFulfilled,onRejected){
 if(typeof onFulfilled !=='function'){
     onFulfilled = value => value;
 }
 if(typeof onRejected  !=='function'){
     onRejected = value => value;
 }
 if(this.status == Promise.FULFILLED){
      onFulfilled(this.value)
 }
 if(this.status == Promise.REJECTED){
     onRejected(this.value)
 }
}
4、在执行then方法时,我们要考虑到传递的函数发生异常的情况,如果函数发生异常,我们应该让它进行错误异常处理,统一交给onRejected来处理错误
then(onFulfilled,onRejected){
    if(typeof onFulfilled !=='function'){
     onFulfilled = value => value;
 }   
 if(typeof onRejected  !=='function'){
     onRejected = value => value;
 }
 if(this.status == Promise.FULFILLED){
     try{onFulfilled(this.value)}catch(error){ onRejected(error) }
 }
 if(this.status == Promise.REJECTED){
     try{onRejected(this.value)}catch(error){ onRejected(error) }
 }
}
5、但是现在我们自己封装的promise有个小问题,我们知道原生的promise中then方法都是异步执行,在一个同步任务执行之后再调用,而我们的现在的情况则是同步调用,因此我们要使用setTimeout来将onFulfilled和onRejected来做异步宏任务执行。
if(this.status=Promise.FULFILLED){
 setTimeout(()=>{
     try{onFulfilled(this.value)}catch(error){onRejected(error)}
 })
}
if(this.status=Promise.REJECTED){
 setTimeout(()=>{
     try{onRejected(this.value)}catch(error){onRejected(error)}
 })
}
现在then方法中,可以处理status为FULFILLED和REJECTED的情况,但是不能处理为pedding的情况,接下来进行几处修改。
6、在构造函数中,添加callbacks来保存pending状态时处理函数,当状态改变时循环调用
constructor(executor) {
    ...
this.callbacks = [];
...
}
7、在then方法中,当status等于pending的情况时,将待执行函数存放到callbacks数组中。
then(onFulfilled,onRejected){
 ...
 if(this.status==Promise.PENDING){
     this.callbacks.push({
         onFulfilled:value=>{
             try {
               onFulfilled(value);
             } catch (error) {
               onRejected(error);
             }
         }
         onRejected: value => {
         try {
           onRejected(value);
         } catch (error) {
           onRejected(error);
         }
       }
     })
 }
 ...
}
8、当执行resolve和reject时,在堆callacks数组中的函数进行执行
resolve(vale){
 if(this.status==Promise.PENDING){
     this.status = Promise.FULFILLED;
     this.value = value;
     this.callbacks.map(callback => {
       callback.onFulfilled(value);
     });
 }
}
reject(value){
 if(this.status==Promise.PENDING){
     this.status = Promise.REJECTED;
     this.value = value;
     this.callbacks.map(callback => {
       callback.onRejected(value);
     });
 }
}
9、then方法中,关于处理pending状态时,异步处理的方法:只需要将resolve与reject执行通过setTimeout定义为异步任务
resolve(value) {
if (this.status == Promise.PENDING) {
    this.status = Promise.FULFILLED;
    this.value = value;
 setTimeout(() => {
   this.callbacks.map(callback => {
     callback.onFulfilled(value);
   });
 });
}
}
reject(value) {
if (this.status == Promise.PENDING) {
    this.status = Promise.REJECTED;
 this.value = value;
 setTimeout(() => {
   this.callbacks.map(callback => {
     callback.onRejected(value);
   });
 });
}
}

到此,promise的then方法的基本实现就结束了。

149. PromisesetTimeout 的区别 ?

参考答案:

JavaScript 将异步任务分为 MacroTask(宏任务) 和 MicroTask(微任务),那么它们区别何在呢?

  1. 依次执行同步代码直至执行完毕;
  2. 检查MacroTask 队列,若有触发的异步任务,则取第一个并调用其事件处理函数,然后跳至第三步,若没有需处理的异步任务,则直接跳至第三步;
  3. 检查MicroTask队列,然后执行所有已触发的异步任务,依次执行事件处理函数,直至执行完毕,然后跳至第二步,若没有需处理的异步任务中,则直接返回第二步,依次执行后续步骤;
  4. 最后返回第二步,继续检查MacroTask队列,依次执行后续步骤;
  5. 如此往复,若所有异步任务处理完成,则结束;

Promise 是一个微任务,主线程是一个宏任务,微任务队列会在宏任务后面执行

setTimeout 返回的函数是一个新的宏任务,被放入到宏任务队列

所以 Promise 会先于新的宏任务执行

150. 如何实现 Promise.all ?

参考答案:

Promise.all 接收一个 promise 对象的数组作为参数,当这个数组里的所有 promise 对象全部变为resolve或 有 reject 状态出现的时候,它才会去调用 .then 方法,它们是并发执行的。

总结 promise.all 的特点

1、接收一个 Promise 实例的数组或具有 Iterator 接口的对象,

2、如果元素不是 Promise 对象,则使用 Promise.resolve 转成 Promise 对象

3、如果全部成功,状态变为 resolved,返回值将组成一个数组传给回调

4、只要有一个失败,状态就变为 rejected,返回值将直接传递给回调 all() 的返回值也是新的 Promise 对象

实现 Promise.all 方法

function promiseAll(promises) {
 return new Promise(function (resolve, reject) {
     if (!isArray(promises)) {
         return reject(new TypeError('arguments must be an array'));
     }
     var resolvedCounter = 0;
     var promiseNum = promises.length;
     var resolvedValues = new Array(promiseNum);
     for (var i = 0; i < promiseNum; i++) {
         (function (i) {
             Promise.resolve(promises[i]).then(function (value) {
                 resolvedCounter++
                 resolvedValues[i] = value
                 if (resolvedCounter == promiseNum) {
                     return resolve(resolvedValues)
                 }
             }, function (reason) {
                 return reject(reason)
             })
         })(i)
     }
 })
}

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

-- EOF --

Comments