JavaScript面试题(三) https://developer.aliyun.com/article/1399381
91.★★ 为什么 JavaScript 是单线程
js作为主要运行在浏览器的脚本语言,js主要用途之一是操作DOM。
举一个栗子,如果js同时有两个线程,同时对同一个dom进行操作,
这时浏览器应该听哪个线程的,如何判断优先级?
为了避免这种问题,js必须是一门单线程语言
92.★★★ 使用箭头函数应该注意什么?
1. 不要在对象里面定义函数,对象里面的行数应该用传统的函数方法
2. 不要在对原型对象上定义函数,在对象原型上定义函数也是遵循着一样的规则
3. 不要用箭头定义构造函数
4. 不要用箭头定义事件回调函数
93.★★★ 你知道 ES6 中的 Generator 和 yiled 吗?在实际开发中使用过吗?
Generator 函数是 ES6 提供的一种异步编程解决方案 执行 Generator 函数会返回一个遍历器对象,可以依次遍历 Generator 函数内部的每一个状态 形式上,Generator函数是一个普通函数,但是有两个特征: 1. function关键字与函数名之间有一个星号 2. 函数体内部使用yield表达式,定义不同的内部状态 /*-----利用Generator函数,在对象上实现Iterator接口-----*/ function* iterEntries(obj) { let keys = Object.keys(obj); for (let i=0; i < keys.length; i++) { let key = keys[i]; yield [key, obj[key]]; } } let myObj = { foo: 3, bar: 7 }; for (let [key, value] of iterEntries(myObj)) { console.log(key, value); }
94.★★★ Cookie、storage 的区别?什么时候使用?
- cookie数据始终在同源的http请求中携带(即使不需要),即cookie在浏览器和服务器间来回传递。localStorage不会自动把数据发给服务器,仅在本地保存。
- **cookie数据还有路径(path)的概念,**可以限制cookie只属于某个路径下。
- 存储大小限制也不同,cookie数据不能超过4k,同时因为每次http请求都会携带cookie,所以cookie只适合保存很小的数据,如会话标识。localStorage 虽然也有存储大小的限制,但比cookie大得多,可以达到5M或更大。
- 数据有效期不同,
localStorage:始终有效,窗口或浏览器关闭也一直保存,因此用作持久数据;
cookie只在设置的cookie过期时间之前一直有效,即使窗口或浏览器关闭。
- WebStorage 支持事件通知机制,可以将数据更新的通知发送给监听者。
- WebStorage 的 api 接口使用更方便。
使用场景:
localStorage可以用来统计页面访问次数。
cookie一般存储用户名密码相关信息,一般使用escape转义编码后存储。
95.★★★ map、fillter、reduce 各自有什么作用?
map 作用是生成一个新数组,遍历原数组,将每个元素拿出来做一些变换然后放入到新的数组中。
另外 map 的回调函数接受三个参数,分别是当前索引元素,索引,原数组
filter 的作用也是生成一个新数组,在遍历数组的时候将返回值为 true 的元素放入新数组,我们可以利用这个函数删除一些不需要的元素
和 map 一样,filter 的回调函数也接受三个参数,用处也相同。
reduce 可以将数组中的元素通过回调函数最终转换为一个值。
它接受两个参数,分别是回调函数和初始值,接下来我们来分解上述代码中 reduce 的过程
首先初始值为 0,该值会在执行第一次回调函数时作为第一个参数传入
回调函数接受四个参数,分别为累计值、当前元素、当前索引、原数组,后三者想必大家都可以明白作用,这里着重分析第一个参数
96.★★ JS的基本数据类型判断有什么方法?
1.typeof: typeof'';// string 有效 typeof 1;// number 有效 typeof Symbol();// symbol 有效 typeof true;//boolean 有效 typeof undefined;//undefined 有效 typeof null;//object 无效 typeof [] ;//object 无效 typeof new Function();// function 有效 typeof new Date();//object 无效 typeof new RegExp();//object 无效 2.instanceof instanceof 是用来判断 A 是否为 B 的实例,表达式为:A instanceof B,如果 A 是 B 的实例,则返回 true,否则返回 false。 在这里需要特别注意的是:instanceof 检测的是原型 3.constructor 当一个函数 F被定义时,JS引擎会为F添加 prototype 原型,然后再在 prototype上添加一个 constructor 属性 4.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] Object.prototype.toString.call(newFunction()) ;// [object Function] Object.prototype.toString.call(newDate()) ;// [object Date] Object.prototype.toString.call([]) ;// [object Array] Object.prototype.toString.call(newRegExp()) ;// [object RegExp] Object.prototype.toString.call(newError()) ;// [object Error] Object.prototype.toString.call(document) ;// [object HTMLDocument] Object.prototype.toString.call(window) ;//[object global] window 是全局对象 global 的引用
97.★★★ 构造函数、实例对象、原型对象三者的关系是什么?
98.★★★★★ JS中的常见设计模式以及应用场景?
1、单例模式
单例模式就是一个实例在整个网页的生命周期里只创建一次,后续再调用实例创建函数的时候,返回的仍是之前创建的实例。在实际开发中应用十分广泛,例如页面中的登录框,显示消息的提示窗
2、策略模式
策略模式是指将策略(算法)封装起来,策略的目的是将算法和使用分离开。
3、代理模式
代理模式很好理解,我们不能直接使用目标函数,而是通过调用代理函数来实现对目标函数的使用。
4、发布订阅模式
发布订阅模式在实际应用中非常常见,例如,我们在微信App上关注了某个公众号,当该公众号有新文章发布时,就会通知我们。
发布订阅模式定义了一种一对多的依赖关系,当“一”发生变化,通知多个依赖。
5、命令模式
所谓命令模式就是将下要执行的业务逻辑封装到一个函数或类中,不需要具体谁来执行该命令的
99.★★ 介绍下事件代理,主要解决什么问题
- 绑定事件太多,浏览器占用内存变大,严重影响性能
- Ajax出现,局部刷新盛行,每次加载完,都要重新绑定事件
- 部分浏览器移除元素时,绑定的事件没有被及时移除,导致内存泄漏,严重影响性能
- Ajax中重复绑定,导致代码耦合性过大,影响后期维护
100.★★★★ 异步的解决方案有哪些?
1.回调函数callback
2.事件发布订阅
3.Promise
4.Generator
5.async/await
101.★★ new 的原理是什么?通过 new 的方式创建对象和通过字面量创建有什么区别?
new操作符的作用如下:
1.创建一个空对象
2.由this变量引用该对象
3.该对象继承该函数的原型
4.把属性和方法加入到this引用的对象中
5.新创建的对象由this引用,最后隐式地返回this。
区别:
字面量创建不会调用 Object构造函数, 简洁且性能更好;
102.★★ 数组去重的方法
/* 方法一: 双层循环,外层循环元素,内层循环时比较值 如果有相同的值则跳过,不相同则push进数组 */ Array.prototype.distinct = function(){ var arr = this, result = [], i, j, len = arr.length; for(i = 0; i < len; i++){ for(j = i + 1; j < len; j++){ if(arr[i] === arr[j]){ j = ++i; } } result.push(arr[i]); } return result; } var arra = [1,2,3,4,4,1,1,2,1,1,1]; arra.distinct(); //返回[3,4,2,1] /* 方法二:利用splice直接在原数组进行操作 双层循环,外层循环元素,内层循环时比较值 值相同时,则删去这个值 注意点:删除元素之后,需要将数组的长度也减1. */ Array.prototype.distinct = function (){ var arr = this, i, j, len = arr.length; for(i = 0; i < len; i++){ for(j = i + 1; j < len; j++){ if(arr[i] == arr[j]){ arr.splice(j,1); len--; j--; } } } return arr; }; var a = [1,2,3,4,5,6,5,3,2,4,56,4,1,2,1,1,1,1,1,1,]; var b = a.distinct(); console.log(b.toString()); //1,2,3,4,5,6,56 /* 优点:简单易懂 缺点:占用内存高,速度慢 方法三:利用对象的属性不能相同的特点进行去重 */ Array.prototype.distinct = function (){ var arr = this, i, obj = {}, result = [], len = arr.length; for(i = 0; i< arr.length; i++){ if(!obj[arr[i]]){ //如果能查找到,证明数组元素重复了 obj[arr[i]] = 1; result.push(arr[i]); } } return result; }; var a = [1,2,3,4,5,6,5,3,2,4,56,4,1,2,1,1,1,1,1,1,]; var b = a.distinct(); console.log(b.toString()); //1,2,3,4,5,6,56 /* 方法四:数组递归去重 运用递归的思想 先排序,然后从最后开始比较,遇到相同,则删除 */ Array.prototype.distinct = function (){ var arr = this, len = arr.length; arr.sort(function(a,b){ //对数组进行排序才能方便比较 return a - b; }) function loop(index){ if(index >= 1){ if(arr[index] === arr[index-1]){ arr.splice(index,1); } loop(index - 1); //递归loop函数进行去重 } } loop(len-1); return arr; }; var a = [1,2,3,4,5,6,5,3,2,4,56,4,1,2,1,1,1,1,1,1,56,45,56]; var b = a.distinct(); console.log(b.toString()); //1,2,3,4,5,6,45,56 //方法五:利用indexOf以及forEach Array.prototype.distinct = function (){ var arr = this, result = [], len = arr.length; arr.forEach(function(v, i ,arr){ //这里利用map,filter方法也可以实现 var bool = arr.indexOf(v,i+1); //从传入参数的下一个索引值开始寻找是否存在重复 if(bool === -1){ result.push(v); } }) return result; }; var a = [1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,3,3,3,3,3,3,3,2,3,3,2,2,1,23,1,23,2,3,2,3,2,3]; var b = a.distinct(); console.log(b.toString()); //1,23,2,3 方法六:利用ES6的set Set数据结构,它类似于数组,其成员的值都是唯一的。 利用Array.from将Set结构转换成数组 function dedupe(array){ return Array.from(new Set(array)); } dedupe([1,1,2,3]) //[1,2,3] 拓展运算符(...)内部使用for...of循环 1 2 3 let arr = [1,2,3,3]; let resultarr = [...new Set(arr)]; console.log(resultarr); //[1,2,3]
103.★★★★ 常见内存泄漏
1、静态集合类,如HashMap、LinkedList等等。如果这些容器为静态的,那么它们的生命周期与程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。简单而言,长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。
2、各种连接,如数据库连接、网络连接和IO连接等。在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用close方法来释放与数据库的连接。只有连接被关闭后,垃圾回收器才会回收对应的对象。否则,如果在访问数据库的过程中,对Connection、Statement或ResultSet不显性地关闭,将会造成大量的对象无法被回收,从而引起内存泄漏。
3、变量不合理的作用域。一般而言,一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏。另一方面,如果没有及时地把对象设置为null,很有可能导致内存泄漏的发生。
4、**内部类持有外部类,**如果一个外部类的实例对象的方法返回了一个内部类的实例对象,这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄露。
5、改变哈希值,当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了,否则,对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为的参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄露
6、缓存泄漏
内存泄漏的另一个常见来源是缓存,一旦你把对象引用放入到缓存中,他就很容易遗忘,对于这个问题,可以使用WeakHashMap代表缓存,此种Map的特点是,当除了自身有对key的引用外,此key没有其他引用那么此map会自动丢弃此值
7、监听器和回调
内存泄漏第三个常见来源是监听器和其他回调,如果客户端在你实现的API中注册回调,却没有显示的取消,那么就会积聚。需要确保回调立即被当作垃圾回收的最佳方法是只保存他的若引用,例如将他们保存成为WeakHashMap中的键。
104.★★★ promise 常见方法和 all 和 race的应用场景
Promise.race():
race的用法:谁跑的快,以谁为准执行回调。
race的使用场景:
比如我们可以用race给某个异步请求设置超时时间,并且在超时后执行相应的操作
Promise.all():
all的用法:谁跑的慢,以谁为准执行回调。
在前端的开发实践中,我们有时会遇到需要发送多个请求并根据请求顺序返回数据的需求
105.★★★ 介绍一下 ES6 中 Set, Map的区别?
Map
在JS中的默认对象的表示方式为{},即一组键值对,但是键必须是字符串。
为了使用Number或者其他数据类型作为键,ES6规范引入了新的数据类型Map。
Map是一组键值对的结构,具有极快的查找速度。初始化Map需要一个二维数组,或者直接初始化一个空Map。
Map 对象是键值对集合,和 JSON 对象类似,但是 key 不仅可以是字符串还可以是其他各种类型的值包括对象都可以成为Map的键
Set
Set也是一组key的集合,与Map类似。但是区别是Set不存储value,并且它的key不能重复。
创建一个Set,需要提供一个Array作为输入,或者直接创建一个空Set:
重复元素会在Set中自动被过滤
Set 对象类似于数组,且成员的值都是唯一的
106.★★ 并行和并发的区别是什么?
并行意味着可以同时取得多个任务,并同时去执行所取得的这些任务。
并行模式相当于将长长的一条队列,划分成了多条短队列,所以并行缩短了任务队列的长度
并发表示多个任务同时都要执行
107.★★★ 为什么操作 dom 慢?
DOM对象本身也是一个js对象,所以严格来说,并不是操作这个对象慢,
而是说操作了这个对象后,需要经过跨流程通信和渲染线程触发的重新渲染,导致DOM操作慢
JS引擎和和渲染引擎的模块化设计,使得它们可以独立优化,
运行速度更快,但是这种设计带来的后果就是DOM操作会越来越慢
108.★★★★ 插入几万个 dom ,如何实现页面不卡顿?
让创建插入节点的工作分批进行: setTimeout(() => { // 插入十万条数据 const total = 100000; // 一次插入 20 条,如果觉得性能不好就减少 const once = 20; // 渲染数据总共需要几次 const loopCount = total / once; let countOfRender = 0 let ul = document.querySelector("ul"); function add() { // 优化性能,插入不会造成回流 const fragment = document.createDocumentFragment(); for (let i = 0; i < once; i++) { const li = document.createElement("li"); li.innerText = Math.floor(Math.random() * total); fragment.appendChild(li); } ul.appendChild(fragment); countOfRender += 1; loop(); } function loop() { if (countOfRender < loopCount) { window.requestAnimationFrame(add); } } loop(); }, 0);
109.★★★ js中的常用事件绑定方法
1. 在DOM元素中直接绑定
2. 在JavaScript代码中绑定
3. 绑定事件监听函数
110.★ 简述 src 和 href 的区别?
src用于替换当前元素,href用于在当前文档和引用资源之间确立联系
111.★★★★ 你知道什么是原型吗?我们为什么要用原型呢?或者说原型为我们提供了什么?
什么是原型: Javascript规定,每一个函数都有一个prototype对象属性,指向另一个对象(原型链上面的)。 prototype(对象属性)的所有属性和方法,都会被构造函数的实例继承。这意味着,我们可以把那些不变(公用)的属性和方法,直接定义在prototype对象属性上。 prototype就是调用构造函数所创建的那个实例对象的原型(proto)。 prototype可以让所有对象实例共享它所包含的属性和方法。也就是说,不必在构造函数中定义对象信息,而是可以直接将这些信息添加到原型中。 为什么要用原型:使用原型对象解决浪费内存
112.★★★ 你了解原型链吗 你能说说 prototype 与 proto 的区别吗?
1.对象有属性__proto__,指向该对象的构造函数的原型对象。
2.方法除了有属性__proto__,还有属性prototype,prototype指向该方法的原型对象。
113.★★★ ts 和 js 的区别
1.ts是静态类语言,可以做到声明即文档,js是动态类语言相对更灵活。
2.如用ts写一个button组件可以清晰的知道,ButtonProps如是否必传,可选,style是什么类型,disabled是什么类型,较js,ts更易于维护和拓展,可以做到代码即注释,避免一个月不见3,代码自己都忘记自己写了什么的尴尬,
4.ts对比js基础类型上,增加了 void/never/any/元组/枚举/以及一些高级类型
5.js没有重载概念,ts有可以重载
6.vscode/ide对ts有很友好的提示
7.ts更利于重构
114.★★★ 简述原生 js 发 ajax 的步骤
1.创建XMLHTTPRequest对象
2.使用open方法设置和服务器的交互信息
3.设置发送的数据,开始和服务器端交互
4.注册事件
5.更新界面
115.★★ 是否所有函数都有 prototype 一说?
1. 使用Function.prototype.bind创建的函数对象 function abc(){console.log('abc')} var binded = abc.bind(null) binded() //abc console.log(binded.prototype) //undefined 2. 箭头函数也没有 var abc = ()=>{console.log('abc')} abc() //abc console.log(abc.prototype) //undefined
116.★★ 为什么 await 在 forEach 中不生效?如何解决?
lodash的forEach和[].forEach不支持await, forEach 只支持同步代码。 解决方法1:使用 for...of 解决方法2:使用 for循环 解决方法3:让orEach支持async await forEach 在正常情况像下面这么写肯定是做不到同步的,程序不会等一个循环中的异步完成再进行下一个循环。原因很明显,在上面的模拟中,while 循环只是简单执行了 callback,所以尽管 callback 内使用了 await ,也只是影响到 callback 内部。 arr.myforeach(async v => { await fetch(v); }); 要支持上面这种写法,只要稍微改一下就好 Array.prototype.myforeach = async function (fn, context = null) { let index = 0; let arr = this; if (typeof fn !== 'function') { throw new TypeError(fn + ' is not a function'); } while (index < arr.length) { if (index in arr) { try { await fn.call(context, arr[index], index, arr); } catch (e) { console.log(e); } } index ++; } };
117.★ a 标签中,如何禁用 href 跳转页面或定位链接?
<a href="#" onclick="return false;">return false;</a> <a href="http://www.baidu.com" onclick="ds(event)">baidu</a> <script> function ds(e){ e.preventDefault(); } </script>
118.★★ 请描述一下 cookies,sessionStorage 和 localStorage 的区别?
1.存储大小
cookie数据大小不能超过4k。
sessionStorage和localStorage 虽然也有存储大小的限制,但比cookie大得多,可以达到5M或更大。
2.有效时间
localStorage 存储持久数据,浏览器关闭后数据不丢失除非主动删除数据;
sessionStorage 数据在当前浏览器窗口关闭后自动删除。
cookie 设置的cookie过期时间之前一直有效,即使窗口或浏览器关闭
3. 数据与服务器之间的交互方式
cookie的数据会自动的传递到服务器,服务器端也可以写cookie到客户端
sessionStorage和localStorage不会自动把数据发给服务器,仅在本地保存。
119.★★★ instanceof的原理是什么?
// instanceof 可以正确的判断对象的类型, //是通过判断对象的原型链中是不是能找到类型的 prototype。 function fn(left, right) { let prototype = right.prototype; left = left.__proto__; while (true) { if (left === undefined || left === null) { return false; } if (left === prototype) { return true; } left = left.__proto__; } }
120. ★★★ 用多种方法实现 JavaScript 继承
1、原型链继承
核心: 将父类的实例作为子类的原型
特点:
1. 非常纯粹的继承关系,实例是子类的实例,也是父类的实例
2. 父类新增原型方法/原型属性,子类都能访问到
3. 简单,易于实现
缺点:
1. 要想为子类新增属性和方法,必须要在new Animal()这样的语句之后执行,不能放到构造器中
2. 无法实现多继承
3. 来自原型对象的所有属性被所有实例共享(来自原型对象的引用属性是所有实例共享的)(详细请看附录代码: 示例1)
4. 创建子类实例时,无法向父类构造函数传参
推荐指数:★★(3、4两大致命缺陷)
2、构造继承
核心:使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)
特点:
1. 解决了1中,子类实例共享父类引用属性的问题
2. 创建子类实例时,可以向父类传递参数
3. 可以实现多继承(call多个父类对象)
缺点:
1. 实例并不是父类的实例,只是子类的实例
2. 只能继承父类的实例属性和方法,不能继承原型属性/方法
3. 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
推荐指数:★★(缺点3)
3、实例继承
核心:为父类实例添加新特性,作为子类实例返回
特点:
1.不限制调用方式,不管是new 子类()还是子类(),返回的对象具有相同的效果
缺点:
1. 实例是父类的实例,不是子类的实例
2. 不支持多继承
推荐指数:★★
4、拷贝继承
特点:支持多继承
缺点:
1. 效率较低,内存占用高(因为要拷贝父类的属性)
2. 无法获取父类不可枚举的方法(不可枚举方法,不能使用for in 访问到)
推荐指数:★(缺点1)
5、组合继承
核心:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用
特点:
1. 弥补了方式2的缺陷,可以继承实例属性/方法,也可以继承原型属性/方法
2. 既是子类的实例,也是父类的实例
3. 不存在引用属性共享问题
4. 可传参
5. 函数可复用
缺点:
1. 调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)
推荐指数:★★★★(仅仅多消耗了一点内存)
6、寄生组合继承
核心:通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点
特点:堪称完美
缺点:实现较为复杂
推荐指数:★★★★★