🧐序言
大家都知道, js
在前端面试中的占比可以说是非常大了。基本上在每一场面试中,有 40% 以上的题都是 js
的题目。 js
不仅考察一个前端人的基础能力,更重要的是前端可以说是以 js
为本,所以也很考察我们的代码能力和逻辑思维。如果说在面试前端中 js
都不过关,那其实还是蛮危险的。
下面的这篇文章中,将讲解我整个秋招备试过程的所有题目。其中,有些知识点是一个很大的范围,但是放在面试系列中整理的话只能是概括性介绍,我将会以链接的方式,将我之前写的文章和其他相关模块的文章,放在题目后进行标注,方便大家更详细的了解当下模块的扩展知识点。
下面开始本文的讲解~📚
🥳思维导图环节
在真正开篇之前,先用一张思维导图来了解全文的内容。详情见下图👇
思维导图收入囊中了,就该开始来架起 js
的知识体系啦~
😏一、JS规范
1、说几条JavaScript的基本规范。
for-in
循环中的变量应该使用let关键字明确限定作用域,从而避免作用域污染。
for(let i in obj){ } 复制代码
- 比较布尔值/数值时,需用
===
/!==
来比较; switch
语句必须带有default
分支;- 不要使用全局函数;
- 使用对象字面量替代
new Array
这种形式,以下给出对象字面量的例子。
let person = { name:'张三', age:13, like:['打篮球','打排球'] } 复制代码
2、对原生JavaScript的了解。
数据类型、运算、对象、 Function
、继承、闭包、作用域、原型链、事件、RegExp
、JSON
、Ajax
、DOM
、BOM
、内存泄漏、异步装载、模板引擎、前端MVC
、路由、模块化、Canvas
、ECMAScript
。
3、说下对JS的了解吧。
是基于原型的动态语言,主要特性有this、原型和原型链。
JS严格意义上来说分为:语言标准部分( ECMAScript
)+ 宿主环境部分。
语言标准部分
2015年
发布ES6
,引入诸多特性,使得能够编写大型项目成为可能,标准自2015年
之后以年号作为代号,每年一更。
宿主环境部分
- 在浏览器宿主环境包括
DOM
+BOM
等 - 在
Node
,宿主环境包括一些文件、数据库、网络、与操作系统的交互等
4、JS原生拖拽节点
- 给需要拖拽的节点绑定
mousedown
,mousemove
,mouseup
事件。 mousedown
事件触发后,开始拖拽。mousemove
时,需要通过event.clientX
和clientY
获取拖拽位置,并实时更新位置。mouseup
时,拖拽结束。- 需要注意浏览器边界值,设置拖拽范围。
5、谈谈你对ES6的理解
- 新增模板字符串(为
JavaScript
提供了简单的字符串插值功能)。 - 箭头函数。
for-of
(用来遍历数据——例如数组中的值)。arguments
对象可以被不确定的参数和默认参数完美替代。ES6
将promise
对象纳入规范,提供了原生的promise
对象。- 增加了
let
和const
命令,用来声明变量。 - 还有就是引入
module
模块的概念。
6、知道ES6的class嘛?
ES6
中的 class
是,为这个类的函数对象直接添加方法,而不是加在这个函数对象的原型对象上。
7、说说你对AMD和Commonjs的理解
CommonJS
是服务器端模块的规范,Node.js
采用了这个规范。CommonJS
规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。AMD
规范则是非同步加载模块,允许指定回调函数。AMD
推荐的风格通过返回一个对象作为模块对象。CommonJS
的风格则是通过对module.exports
或exports
的属性赋值来达到暴露模块对象的目的。
8、如何理解前端模块化
前端模块化就是复杂的文件编程中一个个独立的模块,比如js文件等等,分成独立的模块有利于重用(复用性)和维护(版本迭代),这样会引来模块之间相互依赖的问题,所以有了commonJS规范,AMD,CMD规范等等,以及用于js打包(变异等处理)的工具webpack。
9、面向对象编程思想
- 基本思想是使用对象,类,继承,封装等基本概念来进行程序设计;
- 易维护;
- 易扩展;
- 开发工作的重用性、继承性高,降低重复工作量;
- 缩短了开发周期。
10、用过 TypeScript 吗?它的作用是什么?
TypeScript
为 JS
添加类型支持,以及提供最新版的 ES
语法的支持,有利于团队协作和排错,开发大型项目。
11、PWA使用过吗?serviceWorker的使用原理是啥?
渐进式网络应用(PWA)
是谷歌在 2015年底
提出的概念。基本上算是web应用程序,但在外观和感觉上与 原生app
类似。支持 PWA
的网站可以提供脱机工作、推送通知和设备硬件访问等功能。
Service Worker
是浏览器在后台独立于网页运行的脚本,它打开了通向不需要网页或用户交互的功能的大门。 现在,它们已包括如推送通知和后台同步等功能。 将来, Service Worker
将会支持如定期同步或地理围栏等其他功能。
注:渐进式网络应用 Progressive Network Application
😲二、数据类型
1、问:0.1+0.2 === 0.3吗?为什么?
在正常的数学逻辑思维中, 0.1+0.2=0.3
这个逻辑是正确的,但是在 JavaScript
中 0.1+0.2 !== 0.3
,这是为什么呢?这个问题也会偶尔被用来当做面试题来考查面试者对 JavaScript
的数值的理解程度。
0.1 + 0.2 == 0.3 // false 复制代码
在 JS
中,二进制的浮点数 0.1
和 0.2
并不是精确的,所以它们相加的结果并非正好等于 0.3
,而是一个比较接近 0.3
的数字 0.30000000000000004
,所以条件判断结果为 false
。
原因在于在 JS
当中,采用的是 IEEE 754
的双精度标准,所以计算机内部在存储数据编码的时候,0.1在计算机内部不是精确的 0.1
,而是一个有舍入误差的 0.1
。当代码被编译或解析后, 0.1
已经被四舍五入成一个与之很接近的计算机内部数字,以至于计算还没开始,一个很小的舍入错误就已经产生了。这也就是 0.1 + 0.2
不等于 0.3
的原因。
那如何避免这样的问题?
最常用的方法就是将浮点数转化成整数计算,因为整数都是可以精确表示的。
通常就是把计算数字提升10的N次方倍再除以 10
的 N
次方,一般都用 1000
就行了。
(0.1*1000 + 0.2*1000)/1000 == 0.3 //true 复制代码
2、js数据类型有哪些?具体存在哪里?判断方式是什么?
(1)js数据类型
js
数据类型包括基本数据类型和引用数据类型。
(2)具体存放在哪里?
基本数据类型:
基本数据类型,是指 Numer
、 Boolean
、 String
、 null
、 undefined
、 Symbol
(ES6新增的)、 BigInt(ES2020)
等值,它们在内存中都是存储在栈中的,即直接访问该变量就可以得到存储在栈中的对应该变量的值。
若将一个变量的值赋值给另一个变量,则这两个变量在内存中是独立的,修改其中任意一个变量的值,不会影响另一个变量。这就是基本数据类型。
引用数据类型:
那引用数据类型呢,是指 Object
、 Array
、 Function
等,他们在内存中是存在于栈和堆当中的,即我们要访问到引用类型的值时,需要先访问到该变量在栈中的地址(指向堆中的值),然后再通过这个地址,访问到存放在堆中的数据。这就是引用数据类型。
(3) 常用判断方式:typeof、instanceof、===
1)typeof:
定义:返回数据类型的字符串表达(小写)
用法:typeof + 变量
可以判断:
- undefined/ 数值 / 字符串 / 布尔值 / function (返回 'undefined' / 'number' / 'string' / 'boolean' / 'function')
- null与object 、object与array (null、array、object都会返回 'object' )
<script type="text/javascript"> console.log(typeof "Tony"); // 返回 string console.log(typeof 5.01); // 返回 number console.log(typeof false); // 返回 boolean console.log(typeof undefined); // 返回 undefined console.log(typeof null); // 返回 object console.log(typeof [1,2,3,4]); // 返回 object console.log(typeof {name:'John', age:34}); // 返回 object </script> 复制代码
2)instanceof:
定义:判断对象的具体类型
用法:b instanceof A
→表明 b
是否是 A
的实例对象
可以判断:
专门用来判断对象数据的类型: Object
, Array
与 Function
判断 String
, Number
, Boolean
这三种类型的数据时,直接赋值为 false
,调用构造函数创建的数据为 true
<script type="text/javascript"> let str = new String("hello world") //console.log(str instanceof String); → true str = "hello world" //console.log(str instanceof String); → false let num = new Number(44) //console.log(num instanceof Number); → true num = 44 //console.log(num instanceof Number); → false let bool = new Boolean(true) //console.log(bool instanceof Boolean); → true bool = true //console.log(bool instanceof Boolean); → false </script> 复制代码
<script type="text/javascript"> var items = []; var object = {}; function reflect(value) { return value; } console.log(items instanceof Array); // true console.log(items instanceof Object); // true console.log(object instanceof Object); // true console.log(object instanceof Array); // false console.log(reflect instanceof Function); // true console.log(reflect instanceof Object); // true 复制代码
3)===:
可以判断: undefined
, null
<script type="text/javascript"> let str; console.log(typeof str, str === undefined); //'undefined', true let str2 = null; console.log(typeof str2, str2 === null); // 'object', true </script> 复制代码
3、什么是浅拷贝?什么是深拷贝?说明并分别写出代码。
(1)浅拷贝
所谓浅拷贝,就是一个变量赋值给另一个变量,其中一个变量的值改变,则两个变量的值都变了,即对于浅拷贝来说,是数据在拷贝后,新拷贝的对象内部仍然有一部分数据会随着源对象的变化而变化。
// 分析 function shallowCopy(obj){ let copyObj = {}; for(let i in obj){ copyObj[i] = obj[i]; } return copyObj; } // 实例 let a = { name: '张三', age: 19, like: ['打篮球', '唱歌', '跳舞'] } let b = shallowCopy(a); a.name = '李四'; a.like[0] = '打打乒乓球'; console.log(a); console.log(b); 复制代码
(2)深拷贝
定义:深拷贝 就是,新拷贝的对象内部所有数据都是独立存在的,不会随着源对象的改变而改变。
深拷贝有两种方式:递归拷贝和利用JSON函数进行深拷贝。
- 递归拷贝的实现原理是:对变量中的每个元素进行获取,若遇到基本类型值,直接获取;若遇到引用类型值,则继续对该值内部的每个元素进行获取。
- JSON深拷贝的实现原理是:将变量的值转为字符串形式,然后再转化为对象赋值给新的变量。
局限性:深拷贝的局限性在于,会忽略 undefined
,不能序列化函数,不能解决循环引用的对象。
递归拷贝方式实现代码:
// 分析 function deepCopy(obj){ // 判断是否为引用数据类型 if(typeof obj === 'object'){ let result = obj.constructor === Array ? [] : {}; // 对引用类型继续进行遍历,如果遍历没有结束的话 for(let i in obj){ result[i] = typeof obj[i] === 'object' ? deepCopy(obj[i]) : obj[i]; } return result; } // 为基本数据类型,直接赋值返回 else{ return obj; } } // 实例 - 利用递归函数做深拷贝 let c = { name:'张三', age:12, like:[ '打乒乓球', '打羽毛球', '打太极' ] } let d = deepCopy(c); c.name = '李四'; c.like[0] = '打篮球'; console.log(c); console.log(d); 复制代码
JSON深拷贝实现代码:
// 实例 - 利用json函数做深拷贝 let e = { name: '张三', age: 19, like:['打羽毛球', '唱歌', '跳舞'] } let f = JSON.parse(JSON.stringify(e)); // 注意: JSON函数做深度拷贝时不能拷贝正则,Date,方法函数等 e.name = '李四'; e.like[0] = '打乒乓球'; // console.log(e); // console.log(f); 复制代码
这里可以在参考我之前写过的一篇文章辅助理解👉栈在前端中的应用,顺便再了解下深拷贝和浅拷贝!
4、JS整数是怎么表示的?
JS整数通过 Number
类型来表示,遵循 IEEE 754
标准,通过 64位
来表示一个数字,即 1+11+52
(符号位+指数位+小数部分有效位),最大安全数字是 253 - 1,对应 16位
十进制数。
注:1位十进制数对应4位二进制数
5、Number的存储空间是多大?如果后台发送了一个超过最大数字怎么办?
Math.pow(2,53),53为有效数字;如果后台发送一个超过最大数字,会发生截断,等于 JS
能支持的最大安全数字 253 - 1。
6、NAN是什么,用typeof会输出什么?
Not a Number,表示非数字。
typeof NaN === 'number'; //true 复制代码
7、Symbol有什么用处?
- 可以用来表示一个独一无二的变量,防止命名冲突。
- 除此之外,
Symbol
还可以用来模拟私有属性。
8、null,undefined的区别
undefined
表示不存在这个值。undefined
是一个表示“无”的原始值或者说表示“缺少值”,就是此处应该有一个值,但是还没有定义。尝试读取时就会返回undefined
。- 例如变量被声明了,但没有赋值时,就等于
undefined
。 null
表示一个对象被定义了,值为“空值”。null
是一个对象(空对象,没有任何属性和方法)。- 例如作为函数的参数时,表示该函数的参数不是对象。
- 在验证
null
时,一定要使用===
,因为==
无法区分null
和undefined
。
9、JS隐式转换,显示转换
一般非基础类型进行转换时会调用valueOf,如果 valueOf
无法返回基本类型值,就会调用toString。
(1)字符串和数字
- “+”操作符,如果有一个为字符串,那么都转化到字符串然后执行字符串拼接。
- “-”操作符,转换为数字,相减(-a, a*1, a/1)都能进行隐式强制类型转换。
[] + {} 和 {} + [] 复制代码
(2)布尔值到数字
- 1 + true = 2;
- 1 + false = 1;
(3)转换为布尔值
- for中第二个
- while
- if
- 三元表达式
- || (逻辑或)和 &&(逻辑与)左边的操作个数
(4)符号
- 不能被转换为数字
- 能被转换为布尔值(都是true)
- 可以被转换成字符串“Symbol(cool)”
(5)宽松相等和严格相等
宽松相等允许进行强制类型转换,而严格相等不允许。
①字符串与数字
- 转换为数字然后比较
②其他类型与布尔类型
- 先把布尔类型转换为数字,然后继续进行比较
③对象与非对象
- 执行对象的
ToPrimitive
(对象)然后继续进行比较
④假值列表
- undefined
- null
- false
- +0,-0,NaN
- “”
10、介绍下js有哪些内置对象
Object
是Javascript
中所有对象的父对象;- 其他数据封装类对象:
Object
、Array
、Boolean
、Number
和String
; - 其他对象:
Function
、Arguments
、Math
、Date
、RegExp
、Error
。
11、js有哪些方法定义对象
- 对象字面量:
let obj = {}
; - 构造函数:
let obj = new Object()
; - Object.create():
let obj = Object.create(object.prototype)
;
12、如何判断一个对象是不是空对象?
Object.keys(obj).length === 0 复制代码
13、手写题:获取url参数getUrlParams(url)
//封装函数getUrlParams, 将URL地址的参数解析为对象 function getUrlParams(url){ let obj = {}; if(url.indexOf('?') === -1){ return obj; } let first_res = url.split('?')[1]; let second_res = first_res.split('&'); for(let i in second_res){ third = second_res[i].split('='); obj[third[0]] = third[1]; } return obj; } // 测试代码 let URL = 'https://www.sogou.com/web?ie=UTF-8&query=搜索内容&_em=3'; console.log(getUrlParams(URL)); 复制代码
14、数组能够调用的函数有哪些?
push
向数组尾部添加元素pop
删除并返回数组最后一个元素splice
添加/删除元素slice
返回选定的元素shift
删除第一个元素并返回unshift
向数组开头添加一个或更多元素,并返回新长度sort
对数组元素进行排序find
返回通过测试的数组的第一个元素findIndex
map/filter/reduce
等函数式编程方法- 原型链上的方法:
toString/valueOf
15、函数中的arguments是数组吗?类数组转数组的方法了解一下?
是类数组,是属于鸭子类型的范畴,只是长得像数组。
- ... 运算符
- Array.from
- Array.prototype.slice.apply(arguments)
16、手写题:如何判断数组类型?
// 方法一:instanceof方法 let arr = [1, 2, 3]; console.log(arr instanceof Array); // 方法二:constructor方法 let arr = [1, 2, 3]; console.log(arr.constructor === Array); // 方法三:isArray方法 let arr = [1, 2, 3]; console.log(Array.isArray(arr)); // 方法四:Object.prototype方法 let arr = [1, 2, 3]; console.log(Object.prototype.toString.call(arr) === '[object Array]'); // 方法五:Array.__proto__方法 let arr = [1, 2, 3]; console.log(arr.__proto__ === Array.prototype); // 方法六:Object.getPrototypeOf方法 let arr = [1, 2, 3]; console.log(Object.getPrototypeOf(arr) === Array.prototype); // 方法七:Array.prototype.isPrototypeOf方法 let arr = [1, 2, 3]; console.log(Array.prototype.isPrototypeOf(arr)); 复制代码
17、手写题:sort快速打乱数组
let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; arr.sort(() => Math.random() - 0.5); //利用sort,返回结果为大于等于0时被交换位置,小于0时交换位置。 复制代码
18、手写题:数组去重操作
/* 数组去重:让数组所有元素都独一无二,没有重复元素 */ // 创建一个含有重复元素的数组 let arr = [1, 1, 2, 3, 3, 6, 7, 2, 9, 9] // 第一种方法:利用 Set数据结构 + Array.from() 函数 function removeRepeat1(arr) { return Array.from(new Set(arr)) } // 第二种方法: 利用 Set数据结构 + ...扩展运算符 function removeRepeat2(arr) { return [...new Set(arr)] } // 第三种方法: 利用 indexOf 函数 function removeRepeat3(arr) { let new_arr = [] for(let i in arr) { let item = arr[i] if(new_arr.indexOf(item) === -1) { new_arr.push(item) } } return new_arr } // 第四种方法: 利用 includes 函数 function removeRepeat4(arr) { let new_arr = [] for(let i in arr) { let item = arr[i] if(!new_arr.includes(item)) { new_arr.push(item) } } return new_arr } // 第五种方法: 利用 filter 函数 function removeRepeat5(arr) { return arr.filter((value, index) => { return arr.indexOf(value) === index }) } // 第六种方法: 利用 Map 数据结构 function removeRepeat6(arr) { let map = new Map() let new_arr = [] for(let i in arr) { let item = arr[i] if(!map.has(item)) { map.set(item, true) new_arr.push(item) } } return new_arr } // 测试方法 console.log(removeRepeat1(arr)); console.log(removeRepeat2(arr)); console.log(removeRepeat3(arr)); console.log(removeRepeat4(arr)); console.log(removeRepeat5(arr)); console.log(removeRepeat6(arr)); 复制代码
19、手写题:数组扁平化
/* 数组扁平化就是将多维数组转成一维数组 */ // 多维数组 let arr = [1, 2, [3, 4, [6, 7]]] // 第一种方法:利用 flat() 函数 function flatArr1(arr) { return arr.flat(Infinity) } // 第二种方法: 正则匹配 function flatArr2(arr) { return JSON.parse('[' + JSON.stringify(arr).replace(/\[|\]/g, '') + ']') } // 第三种方法:利用 reduce() 遍历所有的元素 function flatArr3(arr) { return arr.reduce((i, j) => { return i.concat(Array.isArray(j)? flatArr3(j) : j) }, []) } // 第四种方法:直接使用递归函数 function flatArr4(arr) { let new_arr = [] function innerArr(v) { for(let i in v) { let item = v[i] if(Array.isArray(item)) { innerArr(item) } else { new_arr.push(item) } } } innerArr(arr) return new_arr } // 方法测试 console.log(flatArr1(arr)); console.log(flatArr2(arr)); console.log(flatArr3(arr)); console.log(flatArr4(arr)); 复制代码
20、new
操作符具体干了什么呢?
new
一个对象的过程是:
- 创建一个空对象;
- 对新对象进行
[prototype]
绑定(即son. __ proto __ =father.prototype
); - 新对象和函数调用的
this
会绑定起来; - 执行构造函数中的方法;
- 如果函数没有返回值则自动返回这个新对象。
21、手写题:手写一个new方法
function father(name){ this.name = name; this.sayname = function(){ console.log(this.name); } } function myNew(ctx, ...args){ //...args为ES6展开符,也可以使用arguments // 先用Oject创建一个空的对象 let obj = new Object(); // 新对象会执行prototype连接 obj.__proto__ = ctx.prototype; // 新对象和函数调用的this绑定起来 let res = ctx.call(obj, ...args); // 判断函数返回值如果是null或者undefined则返回obj,否则就返回res return res instanceof Object ? res : obj; } let son = myNew(father, 'Bob'); son.sayname(); 复制代码
22、js如何实现继承?
- 原型链继承
- 盗用构造函数继承
- 组合继承
- 原型式继承
- 继承式继承
- 寄生式组合继承
- class的继承
- 详解文章补充👇
- 原文:一文梳理JavaScript中常见的七大继承方案
- 链接:blog.csdn.net/weixin_4480…
- 碎碎念:对于js的继承问题来说,要明确几种继承之间的关系,以及各自的优缺点,还有手写每一种继承。
23、JS中的垃圾回收机制
简单来说,垃圾回收机制就是,清除无用变量,释放更多内存,展现更好性能。
必要性:由于字符串、对象和数组没有固定大小,所有只有当他们的大小已知时,才能对他们进行动态的存储分配。
JavaScript
程序每次创建字符串、数组或对象时,解释器都必须分配内存来存储那个实体。只要像这样动态地分配了内存,最终都要释放这些内存以便他们能够被再用,否则, JavaScript
的解释器将会消耗完系统中所有可用的内存,造成系统崩溃。
这段话解释了为什么系统需要垃圾回收, JS
不像 C/C++
,它有自己的一套垃圾回收机制(GarbageCollection
)。
JavaScript
的解释器可以检测到何时程序不再使用一个对象了,当他确定了一个对象是无用的时候,他就知道不再需要这个对象,可以把它所占用的内存释放掉了。
例如:
var a="hello world"; var b="world"; var a=b; //这时,会释放掉"hello world",释放内存以便再引用 复制代码
垃圾回收的方法:标记清除法、引用计数法。
标记清除法
这是最常见的垃圾回收方式,当变量进入环境时,就标记这个变量为”进入环境“,从逻辑上讲,永远不能释放进入环境的变量所占的内存,只要执行流程进入相应的环境,就可能用到他们。当离开环境时,就标记为“离开环境”。
垃圾回收器在运行的时候会给存储在内存中的变量都加上标记(所有都加),然后去掉环境变量中的变量,以及被环境变量中的变量所引用的变量(条件性去除标记),删除所有被标记的变量,删除的变量无法在环境变量中被访问所以会被删除,最后垃圾回收器完成了内存的清除工作,并回收他们所占用的内存。
引用计数法
另一种不太常见的方法就是引用计数法,引用计数法的意思就是每个值没有引用的次数。当声明了一个变量,并用一个引用类型的值赋值给该变量,则这个值的引用次数为 1
;相反的,如果包含了对这个值引用的变量又取得了另外一个值,则原先的引用值引用次数就减 1
,当这个值的引用次数为 0
的时候,说明没有办法再访问这个值了,因此就把所占的内存给回收进来,这样垃圾收集器再次运行的时候,就会释放引用次数为 0
的这些值。
用引用计数法会存在内存泄露,下面来看原因:
function problem() { var objA = new Object(); var objB = new Object(); objA.someOtherObject = objB; objB.anotherObject = objA; } 复制代码
在这个例子里面, objA
和 objB
通过各自的属性相互引用,这样的话,两个对象的引用次数都为 2
,在采用引用计数的策略中,由于函数执行之后,这两个对象都离开了作用域,函数执行完成之后,因为计数不为 0
,这样的相互引用如果大量存在就会导致内存泄露。
特别是在 DOM
对象中,也容易存在这种问题:
var element=document.getElementById(’‘); var myObj=new Object(); myObj.element=element; element.someObject=myObj; 复制代码
这样就不会有垃圾回收的过程。
🤐三、作用域、原型链、闭包
1、作用域
(1)什么是作用域?
ES5
中只存在两种作用域:全局作用域和函数作用域。在 Javascript
中,我们将作用域定义为一套规则,这套规则用来管理引擎如何在当前作用域以及嵌套子作用域中根据标识符名称进行变量(变量名和函数名)查找。
作用域,就是当访问一个变量时,编译器在执行这段代码时,会首先从当前的作用域中查找是否有这个标识符,如果没有找到,就会去父作用域查找,如果父作用域还没有找到则继续向上查找,直到全局作用域为止。可理解为该上下文中声明的变量和声明的作用范围,可分为块级作用域和函数作用域。
(2)什么是作用域链?
- 作用域链可以看成是将变量对象按顺序连接起来的一条链子。
- 每个执行环境中的作用域都是不同的。
- 当我们引用变量时,会顺着当前执行环境的作用域链,从作用域链的开头开始,依次往上寻找对应的变量,直到找到作用域链的尾部,报错
undefined
。 - 作用域链保证了变量的有序访问。
- 注意:作用域链只能向上访问,到
window
对象即被终止。
2、原型链
(1)什么是原型?什么是原型链?
- 原型和原型链:在
Javascript
中,每个对象都会在其内部初始化一个属性,这个属性就是prototype
(原型)。当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去prototype
里找这个属性,这个prototype
又会有自己的prototype
,于是就这样一直找下去,这样逐级查找形似一个链条,且通过[[prototype]]
属性连接,这个连接的过程被称为原型链。 - 关系:
instance.constructor.prototype === instance.__ proto __
;
(2)什么是原型链继承?
原型链继承,是类比类的继承,即当有两个构造函数 A
和 B
,将一个构造函数 A
的原型对象,通过其 [[prototype]]
属性连接另外一个构造函数B的原型对象时,这个过程被称为原型继承。
(3)手写题:原型链之instance原理
// 判断A是否为B的实例 const instanceOf = (A, B) =>{ // 定义一个指针P指向A let p = A; // 当P存在时则继续执行 while(p){ // 判断P值是否等于B的prototype对象,是则说明A是B的实例 if(p === B.prototype){ return true; } // 不断遍历A的原型链,直到找到B的原型为止 p = p.__proto__; } return false; } console.log(instanceOf([], Array)); 复制代码
3、闭包
(1)闭包是什么?
闭包,是指函数内部再嵌套函数,且在嵌套的函数内有权访问另外一个函数作用域中的变量。
(2)js代码的执行过程
看完闭包的定义,我们再来了解 js
代码的整个执行过程,具体如下:
JavaScript
代码的整个执行过程,分为两个阶段,代码编译阶段与代码执行阶段。编译阶段由编译器完成,将代码翻译成可执行代码,这个阶段作用域规则会确定。执行阶段由引擎完成,主要任务是执行可执行代码,执行上下文在这个阶段创建。
(3)一般如何产生闭包?
- 函数作为返回值被传递
- 函数作为参数被返回
(4)闭包产生的本质
- 当前环境中存在指向父级作用域的引用
(5)闭包的特性
- 函数内再嵌套函数
- 内部函数可以引用外层的参数和变量
- 参数和变量不会被垃圾回收机制回收
(6)闭包的优缺点
- 优点:能够实现封装和缓存等。
- 缺点:①消耗内存;②使用不当会内存溢出。
(7)解决方法
- 在退出函数之前,将不使用的局部变量全部删除。
(8)let闭包
let会产生临时性死区,在当前的执行上下文中,会进行变量提升,但是未被初始化,所以在上下文执行阶段时,执行代码如果还没有执行到变量赋值,就引用此变量会引发报错,因为此变量未被初始化。
(9)闭包的应用场景
- 函数柯里化
- 模块
(10)手写题:函数柯里化
1)柯里化是什么
柯里化指的是,有这样一个函数,它接收函数 A
,并且能返回一个新的函数,这个新的函数能够处理函数 A
的剩余参数。
2)代码实现
下面给出三种具体的实现方式,代码如下:
/** * 函数柯里化:将一个接收多个参数的函数变为接收任意参数返回一个函数的形式,便于之后继续调用,直到最后一次调用,才返回结果值 例子:有一个add函数,用于返回所有参数的和,add(1, 2, 3, 4, 5) 返回的是15 现在要将其变为类似 add(1)(2)(3)(4)(5) 或者 add(1)(2, 3, 4)(5) 的形式,并且功能相同 */ // 普通的add()函数 function add(){ let sum = 0; let args = [...arguments]; for(let i in args){ sum += args[i]; } return sum; } // 第一种add()函数柯里化方式 // 缺点:最后返回的结果是函数类型,但会被隐式转化为字符串,调用toString()方法 function add1(){ // 创建数组,用于存放之后接收的所有参数 let args = [...arguments]; function getArgs(){ args.push(...arguments); return getArgs; } getArgs.toString = function(){ return args.reduce((a,b) => { return a + b; }) } return getArgs; } // 第二种add()函数柯里化方式 // 缺点:需要在最后再自调用一次,即不传参调用表示已没有参数了 function add2(){ let args = [...arguments]; return function(){ // 长度为0时直接把所有数进行相加 if(arguments.length === 0){ return args.reduce((a,b) => { return a + b; }) }else{ // 定义一个_args为了用来遍历 let _args = [...arguments]; // 长度不为0时要进行遍历 for(let i = 0; i < _args.length; i++){ args.push(_args[i]); } return arguments.callee; } } } // 第三种add()函数柯里化方式 // 缺点:在刚开始传参之前,设定总共需要传入参数的个数 function add3(length){ // slice(1)表示从第二个元素开始取值 let args = [...arguments].slice(1); return function(){ args = args.concat([...arguments]); if(arguments.length < length){ return add3.apply(this, [length - arguments.length].concat(args)); }else{ // 返回想要实现的目的 return args.reduce((a,b) => a + b); } } } // 测试代码 let res = add(1,2,3,4,5); let res1 = add1(1)(2)(3)(4)(5); let res2 = add2(1)(2,3,4)(5)(); let res3 = add3(5); console.log(res); console.log(res1); console.log(res2); console.log(res3(1)(2,3)(4)(5)); 复制代码
(11)补充
详解文章补充👇
4、变量对象
(1)变量对象
变量对象,是执行上下文中的一部分,可以抽象为一种数据作用域,也可以理解为就是一个简单的对象,它存储着该执行上下文中的所有变量和函数声明(不包含函数表达式)。
(2)活动对象
活动对象(AO):当变量对象所处的上下文为 active EC
时,成为活动对象。
(3)变量提升
函数在运行的时候,会首先创建执行上下文,然后将执行上下文入栈,当此执行上下文处于栈顶时,开始运行执行上下文。
在创建执行上下文的过程中会做三件事:创建变量对象,创建作用域链,确定 this 指向,其中创建变量对象的过程中,首先会为 arguments
创建一个属性,值为 arguments
,然后会扫描 function
函数声明,创建一个同名属性,值为函数的引用,接着会扫码 var
变量声明,创建一个同名属性,值为 undefined
,这就是变量提升 。
以下给出具体实例:
js (b) //call b console.log(a) //undefined let a = 'Hello World'; function b(){ console.log('call b'); } 复制代码
b(); // call b second function b() { console.log('call b fist'); } function b() { console.log('call b second'); } var b = 'Hello world'; 复制代码
😜四、事件
1、事件模型
W3C中定义事件的发生经历三个阶段:捕获阶段(capturing)、目标阶段(targetin)、冒泡阶段(bubbling)。
- 冒泡型事件:当你使用事件冒泡时,子级元素先触发,父级元素后触发。
- 捕获型事件:当你使用事件捕获时,父级元素先触发,子级元素后触发。
DOM
事件流:同时支持两种事件模型:捕获型事件和冒泡型事件。
2、事件是如何实现的?
基于发布订阅模式,就是在浏览器加载的时候会读取事件相关的代码,但是只有实际等到具体的事件触发的时候才会执行。
比如点击按钮,这是个事件( Event
),而负责处理事件的代码段通常被称为事件处理程序(Event Handler),也就是「启动对话框的显示」这个动作。
在 Web
端,我们常见的就是 DOM
事件:
- DOM0 级事件,直接在
html
元素上绑定on-event
,比如onclick
,取消的话,dom.onclick = null
,同一个事件只能有一个处理程序,后面的会覆盖前面的。 - DOM2 级事件,通过
addEventListener
注册事件,通过removeEventListener
来删除事件,一个事件可以有多个事件处理程序,按顺序执行,捕获事件和冒泡事件。 - DOM3级事件,增加了事件类型,比如
UI
事件,焦点事件,鼠标事件。
3、怎么加事件监听?
通过 onclick 和 addEventListener 来对事件进行监听。
4、什么是事件委托?
(1)定义
- 事件代理(Event Delegation),又称为事件委托,是
Javascript
中常用的绑定事件技巧。 - “事件代理”即是把原本需要绑定的事件委托给父元素,让父元素担当事件监听的职务。
(2)原理
- 事件代理的原理是
DOM
元素的事件冒泡。
(3)好处
- 使用事件代理的好处是可以提高性能。
- 可以大量节省内存占用,减少事件注册,比如说在
ul
上代理所有li
的click
事件。 - 可以实现当新增子对象时无需再次对其进行绑定。
(4)补充
详解文章补充(事件)👇
5、说说事件循环 event loop
(1)定义
首先, js
是单线程的,主要的任务是处理用户的交互,而用户的交互无非就是响应 DOM
的增删改,那如何处理事件响应呢?
浏览器的各种 Web API
会为异步的代码提供了一个单独的运行空间,当异步的代码运行完毕以后,会将代码中的回调送入到 Task Queue
(任务队列)中去,等到调用栈空时,再将队列中的回调函数压入调用栈中执行,等到栈空以及任务队列也为空时,调用栈仍然会不断检测任务队列中是否有代码需要执行,这一过程就是完整的 Event loop
了。
同时需要注意的是,js
引擎在执行过程中有优先级之分, js
引擎在一次事件循环中, 会先执行 js
线程的主任务,然后会去查找是否有微任务microtask(promise)
,如果有那就优先执行微任务,如果没有,再去查找宏任务macrotask(setTimeout、setInterval)
进行执行。
(2)常用的宏任务和微任务
1)常用的宏任务和微任务有:
名称 | 举例(常用) |
宏任务 | script、setTimeout 、setInterval 、setImmediate、I/O、UI Rendering |
微任务 | process.nextTick()、Promise |
上诉的 setTimeout
和 setInterval
等都是任务源,真正进入任务队列的是他们分发的任务。
2)优先级
- setTimeout = setInterval 一个队列
- setTimeout > setImmediate
- process.nextTick > Promise
for(const macroTask of macroTaskQueue){ // 2.再执行宏任务 handleMacroTask(); for(const microTask of microTaskQueue){ // 1.先执行微任务 handleMicroTask(); } } 复制代码
(3)setTimeout(fn,0)多久才执行,Event loop?
setTimeout
按照顺序放到队列里面,然后等待函数调用栈清空之后才开始执行,而这些操作进入队列的顺序,则由设定的延迟时间来决定。
(4)补充
详解文章补充(事件循环)👇