06月24, 2022

四、JavaScript 面试题汇总(151-200)

151. 如何实现 Promise.finally ?

参考答案:

finally 方法是 ES2018 的新特性

finally 方法用于指定不管 Promise 对象最后状态如何,都会执行的操作,执行 thencatch 后,都会执行 finally 指定的回调函数。

方法一:借助 promise.prototype.finally

npm install promise-prototype-finally
const promiseFinally = require('promise.prototype.finally');

// 向 Promise.prototype 增加 finally()
promiseFinally.shim();

// 之后就可以按照上面的使用方法使用了

方法二:实现 Promise.finally

Promise.prototype.finally = function (callback) {
let P = this.constructor;
return this.then(
 value  => P.resolve(callback()).then(() => value),
 reason => P.resolve(callback()).then(() => { throw reason })
);
};

152. 如何判断 img 加载完成

参考答案:

  • img DOM 节点绑定 load 事件
  • readystatechange 事件:readyStatecompleteloaded 则表明图片已经加载完毕。测试 IE6-IE10 支持该事件,其它浏览器不支持。
  • imgcomplete 属性:轮询不断监测 imgcomplete 属性,如果为 true 则表明图片已经加载完毕,停止轮询。该属性所有浏览器都支持。

153. 如何阻止冒泡?

参考答案:

// 方法一:IE9+,其他主流浏览器
event.stopPropagation()
// 方法二:火狐未实现
event.cancelBubble = true;
// 方法三:不建议滥用,jq 中可以同时阻止冒泡和默认事件
return false;

154. 如何阻止默认事件?

参考答案:

// 方法一:全支持
event.preventDefault();
// 方法二:该特性已经从 Web 标准中删除,虽然一些浏览器目前仍然支持它,但也许会在未来的某个时间停止支持,请尽量不要使用该特性。
event.returnValue=false;
// 方法三:不建议滥用,jq 中可以同时阻止冒泡和默认事件
return false;

155. 如何用原生 js 给一个按钮绑定两个 onclick 事件?

参考答案:

使用 addEventListener 方法来绑定事件,就可以绑定多个同种类型的事件。

156. 拖拽会用到哪些事件

参考答案:

在以前,书写一个拖拽需要用到 mousedown、mousemove、mouseup3 个事件。

HTML5 推出后,新推出了一组拖拽相关的 API,涉及到的事件有 dragstart、dragover、drop3 个事件。

157. document.writeinnerHTML 的区别

参考答案:

document.write 是直接写入到页面的内容流,如果在写之前没有调用 document.open, 浏览器会自动调用 open。每次写完关闭之后重新调用该函数,会导致页面全部重绘。

innerHTML 则是 DOM 页面元素的一个属性,代表该元素的 html 内容。你可以精确到某一个具体的元素来进行更改。如果想修改 document 的内容,则需要修改 document.documentElement.innerElement

innerHTML 很多情况下都优于 document.write,其原因在于不会导致页面全部重绘。

158. jQuery 的事件委托方法 bind 、live、delegate、one、on 之间有什么区别?

参考答案:

这几个方法都可以实现事件处理。其中 on 集成了事件处理的所有功能,也是目前推荐使用的方法。

one 是指添加的是一次性事件,意味着只要触发一次该事件,相应的处理方法执行后就自动被删除。

bind 是较早版本的绑定事件的方法,现在已被 on 替代。

livedelegate 主要用来做事件委托。live 的版本较早,现在已被废弃。delegate 目前仍然可用,不过也可用 on 来替代它。

159. $(document).ready 方法和 window.onload 有什么区别?

参考答案:

主要有两点区别:

  1. 执行时机

window.onload 方法是在网页中的所有的元素(包括元素的所有关联文件)都完全加载到浏览器之后才执行。而通过 jQuery 中的$(document).ready方法注册的事件处理程序,只要在 DOM 完全就绪时,就可以调用了,比如一张图片只要<img>标签完成,不用等这个图片加载完成,就可以设置图片的宽高的属性或样式等。

其实从二者的英文字母可以大概理解上面的话,onload 即加载完成,readyDOM 准备就绪。

  1. 注册事件 

$(document).ready方法可以多次使用而注册不同的事件处理程序,而 window.onload 一次只能保存对一个函数的引用,多次绑定函数只会覆盖前面的函数。

160. jquery 中$.get()提交和$.post()提交有区别吗?

参考答案:

相同点:都是异步请求的方式来获取服务端的数据

不同点:

  • 请求方式不同:$.get() 方法使用 GET 方法来进行异步请求的。$.post() 方法使用 POST 方法来进行异步请求的。
  • 参数传递方式不同: GET 请求会将参数跟在 URL 后进行传递,而 POST 请求则是作为 HTTP 消息的实体内容发送给 Web 服务器 的,这种传递是对用户不可见的。
  • 数据传输大小不同: GET 方式传输的数据大小不能超过 2KBPOST 要大的多
  • 安全问题: GET 方式请求的数据会被浏览器缓存起来,因此有安全问题。

161. await async 如何实现 (阿里)

参考答案:

async 函数只是 promise 的语法糖,它的底层实际使用的是 generator,而 generator 又是基于 promise 的。实际上,在 babel 编译 async 函数的时候,也会转化成 generatora 函数,并使用自动执行器来执行它。

实现代码示例:

function asyncToGenerator(generatorFunc) {
 return function() {
   const gen = generatorFunc.apply(this, arguments)
   return new Promise((resolve, reject) => {
     function step(key, arg) {
       let generatorResult
       try {
         generatorResult = gen[key](arg)
       } catch (error) {
         return reject(error)
       }
       const { value, done } = generatorResult
       if (done) {
         return resolve(value)
       } else {
         return Promise.resolve(value).then(val => step('next', val), err => step('throw', err))
       }
     }
     step("next")
   })
 }
}

关于代码的解析,可以参阅:https://blog.csdn.net/xgangzai/article/details/106536325

162. clientWidth,offsetWidth,scrollWidth 的区别

参考答案:

clientWidth = width+左右 padding

offsetWidth = width + 左右 padding + 左右 boder

scrollWidth:获取指定标签内容层的真实宽度(可视区域宽度+被隐藏区域宽度)。

163. 产生一个不重复的随机数组

参考答案:

示例代码如下:

// 生成随机数
function randomNumBoth(Min, Max) {
 var Range = Max - Min;
 var Rand = Math.random();
 var num = Min + Math.round(Rand * Range); //四舍五入
 return num;
}
// 生成数组
function randomArr(len, min, max) {
 if ((max - min) < len) { //可生成数的范围小于数组长度
     return null;
 }
 var hash = [];

 while (hash.length < len) {
     var num = randomNumBoth(min, max);

     if (hash.indexOf(num) == -1) {
         hash.push(num);
     }
 }
 return hash;
}
// 测试
console.log(randomArr(10, 1, 100));

在上面的代码中,我们封装了一个 randomArr 方法来生成这个不重复的随机数组,该方法接收三个参数,len、minmax,分别表示数组的长度、最小值和最大值。randomNumBoth 方法用来生成随机数。

164. continuebreak 的区别

参考答案:

  • break:用于永久终止循环。即不执行本次循环中 break 后面的语句,直接跳出循环。
  • continue:用于终止本次循环。即本次循环中 continue 后面的代码不执行,进行下一次循环的入口判断。

165. 如何在 jquery 上扩展插件,以及内部原理(腾讯)

参考答案:

通过 $.extend(object); 为整个 jQuery 类添加新的方法。

例如:

$.extend({
     sayHello: function(name) {
      console.log('Hello,' + (name ? name : 'World') + '!');
  },
  showAge(){
      console.log(18);
  }
})

// 外部使用
$.sayHello(); // Hello,World!  无参调用
$.sayHello('zhangsan'); // Hello,zhangsan! 带参调用

通过 $.fn.extend(object);jQuery 对象添加方法。

例如:

$.fn.extend({
 swiper: function (options) {
     var obj = new Swiper(options, this); // 实例化 Swiper 对象
     obj.init(); // 调用对象的 init 方法
 }
})

// 外部使用
$('#id').swiper();

extend 方法内部原理

jQuery.extend( target [, object1 ] [, objectN ] )

对后一个参数进行循环,然后把后面参数上所有的字段都给了第一个字段,若第一个参数里有相同的字段,则进行覆盖操作,否则就添加一个新的字段。

解析如下:

// 为与源码的下标对应上,我们把第一个参数称为第0个参数,依次类推
jQuery.extend = jQuery.fn.extend = function() {
 var options, name, src, copy, copyIsArray, clone,
     target = arguments[0] || {}, // 默认第0个参数为目标参数
     i = 1,    // i表示从第几个参数凯斯想目标参数进行合并,默认从第1个参数开始向第0个参数进行合并
     length = arguments.length,
     deep = false;  // 默认为浅度拷贝

 // 判断第0个参数的类型,若第0个参数是boolean类型,则获取其为true还是false
 // 同时将第1个参数作为目标参数,i从当前目标参数的下一个
 // Handle a deep copy situation
 if ( typeof target === "boolean" ) {
     deep = target;

     // Skip the boolean and the target
     target = arguments[ i ] || {};
     i++;
 }

 //  判断目标参数的类型,若目标参数既不是object类型,也不是function类型,则为目标参数重新赋值 
 // Handle case when target is a string or something (possible in deep copy)
 if ( typeof target !== "object" && !jQuery.isFunction(target) ) {
     target = {};
 }

 // 若目标参数后面没有参数了,如$.extend({_name:'wenzi'}), $.extend(true, {_name:'wenzi'})
 // 则目标参数即为jQuery本身,而target表示的参数不再为目标参数
 // Extend jQuery itself if only one argument is passed
 if ( i === length ) {
     target = this;
     i--;
 }

 // 从第i个参数开始
 for ( ; i < length; i++ ) {
     // 获取第i个参数,且该参数不为null,
     // 比如$.extend(target, {}, null);中的第2个参数null是不参与合并的
     // Only deal with non-null/undefined values
     if ( (options = arguments[ i ]) != null ) {

         // 使用for~in获取该参数中所有的字段
         // Extend the base object
         for ( name in options ) {
             src = target[ name ];   // 目标参数中name字段的值
             copy = options[ name ]; // 当前参数中name字段的值

             // 若参数中字段的值就是目标参数,停止赋值,进行下一个字段的赋值
             // 这是为了防止无限的循环嵌套,我们把这个称为,在下面进行比较详细的讲解
             // Prevent never-ending loop
             if ( target === copy ) {
                 continue;
             }

             // 若deep为true,且当前参数中name字段的值存在且为object类型或Array类型,则进行深度赋值
             // Recurse if we're merging plain objects or arrays
             if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) {
                 // 若当前参数中name字段的值为Array类型
                 // 判断目标参数中name字段的值是否存在,若存在则使用原来的,否则进行初始化
                 if ( copyIsArray ) {
                     copyIsArray = false;
                     clone = src && jQuery.isArray(src) ? src : [];

                 } else {
                     // 若原对象存在,则直接进行使用,而不是创建
                     clone = src && jQuery.isPlainObject(src) ? src : {};
                 }

                 // 递归处理,此处为2.2
                 // Never move original objects, clone them                      
                 target[ name ] = jQuery.extend( deep, clone, copy );

             // deep为false,则表示浅度拷贝,直接进行赋值
             // 若copy是简单的类型且存在值,则直接进行赋值
             // Don't bring in undefined values
             } else if ( copy !== undefined ) {
                 // 若原对象存在name属性,则直接覆盖掉;若不存在,则创建新的属性
                 target[ name ] = copy;
             }
         }
     }
 }

 // 返回修改后的目标参数
 // Return the modified object
 return target;
};

166. async/await 如何捕获错误

参考答案:

可以使用 try...catch 来进行错误的捕获

示例代码:

async function test() {
 try {
     const res = await test1()
 } catch (err) {
     console.log(err)
 }
 console.log("test")
}

167. Proxy 对比 Object.defineProperty 的优势

参考答案:

Proxy 的优势如下:

  • Object.defineProperty 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历,而 Proxy 可以直接监听对象而非属性;
  • Object.defineProperty 无法监控到数组下标的变化,而 Proxy 可以直接监听数组的变化;
  • Proxy 有多达 13 种拦截方法;
  • Proxy 作为新标准将受到浏览器厂商重点持续的性能优化;

168. 原型链,可以改变原型链的规则吗?

参考答案:

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

我们可以通过手动赋值的方式来改变原型链所对应的原型对象。

169. 讲一讲继承的所有方式都有什么?手写一个寄生组合式继承

参考答案:

可以参阅前面第 9、18、47 题答案。

其中圣杯模式就是寄生组合式继承。

170. JS 基本数据类型有哪些?栈和堆有什么区别,为什么要这样存储。(快手)

参考答案:

关于 JS 基本数据类型有哪些这个问题,可以参阅前面 26 题。

栈和堆的区别在于堆是动态分配内存,内存大小不一,也不会自动释放。栈是自动分配相对固定大小的内存空间,并由系统自动释放。

js 中,基本数据都是直接按值存储在栈中的,每种类型的数据占用的内存空间的大小是确定的,并由系统自动分配和自动释放。这样带来的好处就是,内存可以及时得到回收,相对于堆来说,更加容易管理内存空间。

js 中其他类型的数据被称为引用类型的数据(如对象、数组、函数等),它们是通过拷贝和 new 出来的,这样的数据存储于堆中。其实,说存储于堆中,也不太准确,因为,引用类型的数据的地址指针是存储于栈中的,当我们想要访问引用类型的值的时候,需要先从栈中获得对象的地址指针,然后,在通过地址指针找到堆中的所需要的数据。

171. setTimeout(() => {}, 0) 什么时候执行

参考答案:

因为 setTimeout 是异步代码,所以即使后面的时间为 0,也要等到同步代码执行完毕后才会执行。

172. js 有函数重载吗(网易)

参考答案:

所谓函数重载,是方法名称进行重用的一种技术形式,其主要特点是“方法名相同,参数的类型或个数不相同”,在调用时会根据传递的参数类型和个数的不同来执行不同的方法体。

JS 中,可以通过在函数内容判断形参的类型或个数来执行不同的代码块,从而达到模拟函数重载的效果。

173. 给你一个数组,计算每个数出现的次数,如果每个数组返回的数都是独一无二的就返回 true 相反则返回的 flase

参考答案:

输入:arr = [1,2,2,1,1,3]

输出:true

解释:在该数组中,1 出现了 3 次,2 出现了 2 次,3 只出现了 1 次。没有两个数的出现次数相同。

代码示例:

function uniqueOccurrences(arr) {
 let uniqueArr = [...new Set(arr)]
 let countArr = []
 for (let i = 0; i < uniqueArr.length; i++) {
     countArr.push(arr.filter(item => item == uniqueArr[i]).length)
 }
 return countArr.length == new Set(countArr).size
};

// 测试
console.log(uniqueOccurrences([1, 2, 2, 1, 1, 3])); // true
console.log(uniqueOccurrences([1, 2, 2, 1, 1, 3, 2])); // false

174. 封装一个能够统计重复的字符的函数,例如 aaabbbdddddfff 转化为 3a3b5d3f

参考答案:

function compression(str) {
 if (str.length == 0) {
     return 0;
 }
 var len = str.length;
 var str2 = "";
 var i = 0;
 var num = 1;
 while (i < len) {
     if (str.charAt(i) == str.charAt(i + 1)) {
         num++;
     } else {
         str2 += num;
         str2 += str.charAt(i);
         num = 1;
     }
     i++;
 }
 return str2;
}
// 测试:
console.log(compression('aaabbbdddddfff')); // 3a3b5d3f

175. 写出代码的执行结果,并解释为什么?

function a() {
    console.log(1);
}
(function() {
    if (false) {
        function a() {
            console.log(2);
        }
    }
    console.log(typeof a); 
    a(); 
})()

参考答案:

会报错,a is not a function

因为立即执行函数里面有函数 aa 会被提升到该函数作用域的最顶端,但是由于判断条件是 false,所以不会进入到条件语句里面, a 也就没有值。所以 typeof 打印出来是 undefined。而后面在尝试调用方法,自然就会报错。

176. 写出代码的执行结果,并解释为什么?

alert(a);
a();
var a = 3;
function a() {
  alert(10);
};
alert(a);
a = 6;
a();

参考答案:

首先打印 function a() {alert(10);};

然后打印 10

最后打印 3

解析:

首先 a 变量会被提升到该全局作用域的最顶端,然后值为对应的函数,所以第一次打印出来的是函数。

接下来调用这个 a 函数,所以打印出 10

最后给这个 a 赋值为 3,然后又 alert,所以打印出 3。

之后 a 的值还会发生改变,但是由于没有 alert,说明不会再打印出其他值了。

177. 写出下面程序的打印顺序,并简要说明原因

setTimeout(function () {
    console.log("set1");
    new Promise(function (resolve) {
        resolve();
    }).then(function () {
        new Promise(function (resolve) {
            resolve();
        }).then(function () {
            console.log("then4");
        })
        console.log('then2');
    })
});
new Promise(function (resolve) {
    console.log('pr1');
    resolve();
}).then(function () {
    console.log('then1');
});

setTimeout(function () {
    console.log("set2");
});
console.log(2);

new Promise(function (resolve) {
    resolve();
}).then(function () {
    console.log('then3');
})

参考答案:

打印结果为:

pr1 2 then1 then3 set1 then2 then4 set2

178. javascript 中什么是伪数组?如何将伪数组转换为标准数组

参考答案:

JavaScript 中,arguments 就是一个伪数组对象。关于 arguments 具体可以参阅后面 250 题。

可以使用 ES6 的扩展运算符来将伪数组转换为标准数组

例如:

var arr = [...arguments];

179. arrayobject 的区别

参考答案:

数组表示有序数据的集合,对象表示无序数据的集合。如果数据顺序很重要的话,就用数组,否则就用对象。

180. jquery 事件委托

参考答案:

jquery 中使用 on 来绑定事件的时候,传入第二个参数即可。例如:

$("ul").on("click","li",function () {
alert(1);
})

181. JS 基本数据类型

参考答案:

请参阅前面第 26

182. 请实现一个模块 math,支持链式调用math.add(2,4).minus(3).times(2);

参考答案:

示例代码:

class Math {
 constructor(value) {
     let hasInitValue = true;
     if (value === undefined) {
         value = NaN;
         hasInitValue = false;
     }
     Object.defineProperties(this, {
         value: {
             enumerable: true,
             value: value,
         },
         hasInitValue: {
             enumerable: false,
             value: hasInitValue,
         },
     });
 }

 add(...args) {
     const init = this.hasInitValue ? this.value : args.shift();
     const value = args.reduce((pv, cv) => pv + cv, init);
     return new Math(value);
 }

 minus(...args) {
     const init = this.hasInitValue ? this.value : args.shift();
     const value = args.reduce((pv, cv) => pv - cv, init);
     return new Math(value);
 }

 times(...args) {
     const init = this.hasInitValue ? this.value : args.shift();
     const value = args.reduce((pv, cv) => pv * cv, init);
     return new Math(value);
 }

 divide(...args) {
     const init = this.hasInitValue ? this.value : args.shift();
     const value = args.reduce((pv, cv) => pv / cv, init);
     return new Math(value);
 }

 toJSON() {
     return this.valueOf();
 }

 toString() {
     return String(this.valueOf());
 }

 valueOf() {
     return this.value;
 }

 [Symbol.toPrimitive](hint) {
     const value = this.value;
     if (hint === 'string') {
         return String(value);
     } else {
         return value;
     }
 }
}

export default new Math();

183. 请简述 ES6 代码转成 ES5 代码的实现思路。

参考答案:

说到 ES6 代码转成 ES5 代码,我们肯定会想到 Babel。所以,我们可以参考 Babel 的实现方式。

那么 Babel 是如何把 ES6 转成 ES5 呢,其大致分为三步:

  • 将代码字符串解析成抽象语法树,即所谓的 AST
  • AST 进行处理,在这个阶段可以对 ES6 代码进行相应转换,即转成 ES5 代码
  • 根据处理后的 AST 再生成代码字符串

184. 下列代码的执行结果

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
async function async2() {
    console.log('async2');
}
console.log('script start');
setTimeout(function () {
    console.log('setTimeout');
}, 0);
async1();
new Promise(function (resolve) {
    console.log('promise1');
    resolve();
}).then(function () {
    console.log('promise2');
});
console.log('script end');

参考答案:

script start async1 start async2 promise1 script end async1 end promise2 setTimeout

解析:

在此之前我们需要知道以下几点:

  • setTimeout 属于宏任务
  • Promise 本身是同步的立即执行函数,Promise.then 属于微任务
  • async 方法执行时,遇到 await 会立即执行表达式,表达式之后的代码放到微任务执行

第一次执行:执行同步代码

Tasks(宏任务):run script、 setTimeout callback
Microtasks(微任务):awaitPromise then
JS stack(执行栈): script
Log: script start、async1 start、async2、promise1、script end

第二次执行:执行宏任务后,检测到微任务队列中不为空、一次性执行完所有微任务

Tasks(宏任务):run script、 setTimeout callback
Microtasks(微任务):Promise then
JS stack(执行栈): await
Log: script start、async1 start、async2、promise1、script end、async1 end、promise2

第三次执行:当微任务队列中为空时,执行宏任务,执行setTimeout callback,打印日志。

Tasks(宏任务):null
Microtasks(微任务):null
JS stack(执行栈):setTimeout callback
Log: script start、async1 start、async2、promise1、script end、async1 end、promise2、setTimeout

185. JS 有哪些内置对象?

参考答案:

数据封装类对象:String,Boolean,Number,ArrayObject

其他对象:Function,Arguments,Math,Date,RegExp,Error

186. DOM 怎样添加、移除、移动、复制、创建和查找节点

参考答案:

请参阅前面 121 题。

187. eval 是做什么的?

参考答案:

此函数可以接受一个字符串 str 作为参数,并把此 str 当做一段 javascript 代码去执行,如果 str 执行结果是一个值则返回此值,否则返回 undefined。如果参数不是一个字符串,则直接返回该参数。

例如:

eval("var a=1");//声明一个变量a并赋值1。
eval("2+3");//5执行加运算,并返回运算值。
eval("mytest()");//执行mytest()函数。
eval("{b:2}");//声明一个对象。

188. nullundefined 的区别?

参考答案:

请参阅前面第 29 题。

189. new 操作符具体干了什么呢?

参考答案:

  • 创建一个空对象 。
  • 由 this 变量引用该对象 。
  • 该对象继承该函数的原型(更改原型链的指向) 。
  • 把属性和方法加入到 this 引用的对象中。
  • 新创建的对象由 this 引用 ,最后隐式地返回 this,过程如下:
var obj = {};
obj.__proto__ = Base.prototype;
Base.call(obj);

190. 去除字符串中的空格

参考答案:

方法一:replace正则匹配方法

代码示例:

  • 去除字符串内所有的空格:str = str.replace(/\s*/g,"");
  • 去除字符串内两头的空格:str = str.replace(/^\s*|\s*$/g,"");
  • 去除字符串内左侧的空格:str = str.replace(/^\s*/,"");
  • 去除字符串内右侧的空格:str = str.replace(/(\s*$)/g,"");

方法二:字符串原生 trim 方法

trim 方法能够去掉两侧空格返回新的字符串,不能去掉中间的空格

191. 常见的内存泄露,以及解决方案

参考答案:

内存泄露概念

内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。

内存泄漏通常情况下只能由获得程序源代码和程序员才能分析出来。然而,有不少人习惯于把任何不需要的内存使用的增加描述为内存泄漏,即使严格意义上来说这是不准确的。

JS 垃圾收集机制

JS 具有自动回收垃圾的机制,即执行环境会负责管理程序执行中使用的内存。在C和C++等其他语言中,开发者的需要手动跟踪管理内存的使用情况。在编写 JS 代码的时候,开发人员不用再关心内存使用的问题,所需内存的分配 以及无用的回收完全实现了自动管理。

Js中最常用的垃圾收集方式是标记清除(mark-and-sweep)。当变量进入环境(例如,在函数中声明一个变量)时,就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占的内存,因为只要执行流进入相应的环境,就可能用到它们。而当变量离开环境时,这将其 标记为“离开环境”。

常见内存泄漏以及解决方案

  1. 意外的全局变量

Js处理未定义变量的方式比较宽松:未定义的变量会在全局对象创建一个新变量。在浏览器中,全局对象是window。

function foo(arg) { 
    bar = "this is a hidden global variable"; //等同于window.bar="this is a hidden global variable"
    this.bar2= "potential accidental global";//这里的this 指向了全局对象(window),等同于window.bar2="potential accidental global"
}

解决方法:在 JavaScript 程序中添加,开启严格模式'use strict',可以有效地避免上述问题。

注意:那些用来临时存储大量数据的全局变量,确保在处理完这些数据后将其设置为null或重新赋值。与全局变量相关的增加内存消耗的一个主因是缓存。缓存数据是为了重用,缓存必须有一个大小上限才有用。高内存消耗导致缓存突破上限,因为缓 存内容无法被回收。

  1. 循环引用

在js的内存管理环境中,对象 A 如果有访问对象 B 的权限,叫做对象 A 引用对象 B。引用计数的策略是将“对象是否不再需要”简化成“对象有没有其他对象引用到它”,如果没有对象引用这个对象,那么这个对象将会被回收 。

let obj1 = { a: 1 }; // 一个对象(称之为 A)被创建,赋值给 obj1,A 的引用个数为 1   
let obj2 = obj1; // A 的引用个数变为 2  

obj1 = 0; // A 的引用个数变为 1  
obj2 = 0; // A 的引用个数变为 0,此时对象 A 就可以被垃圾回收了

但是引用计数有个最大的问题: 循环引用。

function func() {  
    let obj1 = {};  
    let obj2 = {};  

    obj1.a = obj2; // obj1 引用 obj2  
    obj2.a = obj1; // obj2 引用 obj1  

}

当函数 func 执行结束后,返回值为 undefined,所以整个函数以及内部的变量都应该被回收,但根据引用计数方法,obj1 和 obj2 的引用次数都不为 0,所以他们不会被回收。要解决循环引用的问题,最好是在不使用它们的时候手工将它们设为空。上面的例子可以这么做:

obj1 = null;  
obj2 = null;
  1. 被遗忘的计时器和回调函数
let someResource = getData();  
setInterval(() => {  
    const node = document.getElementById('Node');  
    if(node) {  
        node.innerhtml = JSON.stringify(someResource));  
    }  
}, 1000);

上面的例子中,我们每隔一秒就将得到的数据放入到文档节点中去。

但在 setInterval 没有结束前,回调函数里的变量以及回调函数本身都无法被回收。那什么才叫结束呢?

就是调用了 clearInterval。如果回调函数内没有做什么事情,并且也没有被 clear 掉的话,就会造成内存泄漏。

不仅如此,如果回调函数没有被回收,那么回调函数内依赖的变量也没法被回收。上面的例子中,someResource 就没法被回收。同样的,setTiemout 也会有同样的问题。所以,当不需要 interval 或者 timeout 时,最好调用 clearInterval 或者 clearTimeout

  1. DOM 泄漏

JS 中对DOM操作是非常耗时的。因为JavaScript/ECMAScript引擎独立于渲染引擎,而DOM是位于渲染引擎,相互访问需要消耗一定的资源。 而 IEDOM 回收机制便是采用引用计数的,以下主要针对 IE 而言的。

a. 没有清理的 DOM 元素引用

var refA = document.getElementById('refA');
document.body.removeChild(refA);
// refA 不能回收,因为存在变量 refA 对它的引用。将其对 refA 引用释放,但还是无法回收 refA。

解决办法:refA = null;

b. 给 DOM 对象添加的属性是一个对象的引用

var MyObject = {}; 
document.getElementById('mydiv').myProp = MyObject;

解决方法: 在 window.onunload 事件中写上: document.getElementById('mydiv').myProp = null;

c. DOM 对象与 JS 对象相互引用

function Encapsulator(element) { 
    this.elementReference = element; 
    element.myProp = this; 
} 
new Encapsulator(document.getElementById('myDiv'));

解决方法: 在 onunload 事件中写上: document.getElementById('myDiv').myProp = null;

d. 给 DOM 对象用 attachEvent 绑定事件

function doClick() {} 
element.attachEvent("onclick", doClick);

解决方法: 在onunload事件中写上: element.detachEvent('onclick', doClick);

e. 从外到内执行 appendChild。这时即使调用 removeChild 也无法释放

var parentDiv = document.createElement("div"); 
var childDiv = document.createElement("div"); 
document.body.appendChild(parentDiv); 
parentDiv.appendChild(childDiv);

解决方法: 从内到外执行 appendChild:

var parentDiv = document.createElement("div"); 
var childDiv = document.createElement("div"); 
parentDiv.appendChild(childDiv); 
document.body.appendChild(parentDiv);
  1. JS 的闭包

闭包在 IE6 下会造成内存泄漏,但是现在已经无须考虑了。值得注意的是闭包本身不会造成内存泄漏,但闭包过多很容易导致内存泄漏。闭包会造成对象引用的生命周期脱离当前函数的上下文,如果闭包如果使用不当,可以导致环形引用(circular reference),类似于死锁,只能避免,无法发生之后解决,即使有垃圾回收也还是会内存泄露。

  1. console

控制台日志记录对总体内存配置文件的影响可能是许多开发人员都未想到的极其重大的问题。记录错误的对象可以将大量数据保留在内存中。注意,这也适用于:

(1) 在用户键入 JavaScript 时,在控制台中的一个交互式会话期间记录的对象。 (2) 由 console.log 和 console.dir 方法记录的对象。

192. 箭头函数和普通函数里面的 this 有什么区别

参考答案:

请参阅前面第 24、25

193. 设计⼀个⽅法(isPalindrom)以判断是否回⽂(颠倒后的字符串和原来的字符串⼀样为回⽂)

参考答案:

示例代码如下:

function isPalindrome(str) {
 if (typeof str !== 'string') {
     return false
 }
 return str.split('').reverse().join('') === str
}

// 测试
console.log(isPalindrome('HelleH')); // true
console.log(isPalindrome('Hello')); // false

194. 设计⼀个⽅法(findMaxDuplicateChar)以统计字符串中出现最多次数的字符

参考答案:

示例代码如下:

function findMaxDuplicateChar(str) {
 let cnt = {},    //用来记录所有的字符的出现频次
     c = '';        //用来记录最大频次的字符
 for (let i = 0; i < str.length; i++) {
     let ci = str[i];
     if (!cnt[ci]) {
         cnt[ci] = 1;
     } else {
         cnt[ci]++;
     }
     if (c == '' || cnt[ci] > cnt[c]) {
         c = ci;
     }
 }
 console.log(cnt); // { H: 1, e: 1, l: 3, o: 2, ' ': 1, W: 1, r: 1, d: 1 }
 return c;
}

// 测试
console.log(findMaxDuplicateChar('Hello World')); // l

195. 设计⼀段代码,使得通过点击按钮可以在 span 中显示⽂本框中输⼊的值

参考答案:

示例代码如下:

<body>
 <span id="showContent">在右侧输入框中输入内容</span>
 <input type="text" name="content" id="content">
 <button id="btn">更新内容</button>
 <script>
     btn.onclick = function(){
         var content = document.getElementById('content').value;
         if(content){
             document.getElementById('showContent').innerHTML = content;
         }
     }
 </script>
</body>

196. mapforEach 的区别?

参考答案:

两者区别

forEach()方法不会返回执行结果,而是undefined

也就是说,forEach()会修改原来的数组。而map()方法会得到一个新的数组并返回。

适用场景

forEach适合于你并不打算改变数据的时候,而只是想用数据做一些事情 – 比如存入数据库或则打印出来。

map()适用于你要改变数据值的时候。不仅仅在于它更快,而且返回一个新的数组。这样的优点在于你可以使用复合(composition)(map, filter, reduce 等组合使用)来玩出更多的花样。

197. Array 的常用方法

参考答案:

Array 的常用方法很多,挑选几个自己在实际开发中用的比较多的方法回答即可。 更多 Array 相关用法可以参阅:https://www.w3school.com.cn/jsref/jsref_obj_array.asp

198. 数组去重的多种实现方式

参考答案:

请参阅前面第 2 题答案。

199. 什么是预解析(预编译)

参考答案:

所谓的预解析(预编译)就是:在当前作用域中,JavaScript 代码执行之前,浏览器首先会默认的把所有带 varfunction 声明的变量进行提前的声明或者定义。

另外,var 声明的变量和 function 声明的函数在预解析的时候有区别,var 声明的变量在预解析的时候只是提前的声明,function 声明的函数在预解析的时候会提前声明并且会同时定义。也就是说 var 声明的变量和 function 声明的函数的区别是在声明的同时有没有同时进行定义。

200. 原始值类型和引用值类型的区别是什么?

参考答案:

可以参阅前面第 26

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

-- EOF --

Comments