不足之处评论我进行修改,或者有更好的面试题评论告诉我,我添加上,后续我还会继续添加面试题
1、什么是闭包
- 必须有一个内嵌函数。
- 内嵌函数必须引用外部函数中的变量。
- 外部函数的返回值必须是内嵌函数。
闭包是这样的一种机制:函数嵌套函数,内部函数可以引用外部函数的参数和变量。参数和变量不会被垃圾回收机制收回。
2.闭包的作用
- 数据封装:闭包可以用于封装私有数据,只暴露有限的接口供外界访问。
- 保持变量状态:闭包允许函数记住和访问其词法作用域中的变量,即使函数在其作用域之外执行。
- 延迟计算:通过闭包可以推迟计算的执行,直到真正需要结果时。
相比全局变量和局部变量,闭包有两大特点:
1.闭包拥有全局变量的不被释放的特点
2.闭包拥有局部变量的无法被外部访问的特点
闭包的好处:
1.可以让一个变量长期在内存中不被释放
2.避免全局变量的污染,和全局变量不同,闭包中的变量无法被外部使用
3.私有成员的存在,无法被外部调用,只能直接内部调用
3.闭包的使用场景
- 装饰器:在不修改原有函数代码的情况下,增加额外的功能。
- 回调函数:封装了状态的函数可以作为回调函数传递给某些操作。
- 函数工厂:根据输入参数的不同返回不同行为的函数。
4.闭包语法规范
- 定义外部函数。
- 在外部函数内定义内部函数。
- 内部函数引用外部函数的变量。
- 外部函数返回内部函数。
5.闭包的注意事项
1.内存泄漏
由于闭包可以访问外部函数的作用域,如果不小心,可能导致内存泄漏。在不再需要使用闭包时,及时释放对它的引用是很重要的。
2.陷阱:异步操作
当在循环或迭代中创建闭包时,可能会遇到与预期不符的问题,尤其是在涉及异步操作时。
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
// 输出: 5 5 5 5 5
在上面的例子中,由于 JavaScript 中的事件循环机制,setTimeout 中的闭包在循环结束后执行,此时 i 的值已经变成了 5。为了解决这个问题,可以使用块级作用域或立即执行函数。
for (var i = 0; i < 5; i++) {
(function(index) {
setTimeout(function() {
console.log(index);
}, 1000);
})(i);
}
// 输出: 0 1 2 3 4
2.垃圾回收机制有两种:1.标记清除, 2.引用计数
1.标记清除:js会对变量做一个标记yes or no的标签以供js引擎来处理,当变量在某个环境下被使用则标记为yes,当超出改环境(可以理解为超出作用域)则标记为no,然后对有no的标签进行释放。
2.引用计数:对于js中引用类型的变量, 采用引用计数的内存回收机制,当一个引用类型的变量赋值给另一个变量时, 引用计数会+1, 而当其中有一个变量不再等于值时,引用计数会-1, 如果引用计数为0, 则js引擎会将其释放掉。
3.防抖节流的概念
防抖 是指在一定时间内,在动作被连续频繁触发的情况下,动作只会被执行一次,也就是说当调用动作过n毫秒后,才会执行该动作,若在这n毫秒内又调用此动作则将重新计算执行时间,所以短时间内的连续动作永远只会触发一次,比如说用手指一直按住一个弹簧,它将不会弹起直到你松手为止。
节流 是指一定时间内执行的操作只执行一次,也就是说即预先设定一个执行周期,当调用动作的时刻大于等于执行周期则执行该动作,然后进入下一个新周期,一个比较形象的例子是人的眨眼睛,就是一定时间内眨一次。
1.作用
节流(throttle)与 防抖(debounce)都是为了限制函数的执行频次,以优化函数触发频率过高导致的响应速度跟不上触发频率,出现延迟,假死或卡顿的现象。
2. 区别
防抖: 任务频繁触发的情况下,只有任务触发的间隔超过指定间隔的时候,任务才会执行;
节流: 指定时间间隔内只会执行一次任务;
3.原理
函数节流与函数防抖巧妙地使用 setTimeout 来存放待执行的函数,这样可以很方便的利用 clearTimeout 在合适的时机来清除待执行的函数。
4.应用场景
函数节流应用的实际场景,多数在监听页面元素滚动事件的时候会用到。因为滚动事件,是一个高频触发的事件。
函数防抖的应用场景,最常见的就是用户注册时候的手机号码验证和邮箱验证了。只有等用户输入完毕后,前端才需要检查格式是否正确,如果不正确,再弹出提示语。
4.重绘与回流的区别
1、 重绘:元素样式的改变(但宽高、大小、位置等不变)
如:outline、visibility、color、background-color等
只改变自身样式,不会影响到其他元素
2、 回流:元素的大小或者位置发生改变(当页面布局和几何信息发生改变的时候),触发了重新布局导致渲染树重新计算布局和渲染
如添加或删除可见的DOM元素;元素的位置发生变化;元素的尺寸发生变化、内容发生变化(如文本变化或图片被另一个不同尺寸的图片所代替);页面一开始渲染的时候(无法避免);
因为回流是根据视口大小来计算元素的位置和大小的,所以浏览器窗口尺寸变化也会引起回流
注意:回流一定会触发重绘,而重绘不一定会回流
3.如何避免(减少)回流
css
避免设置多层内联样式。
如果需要设置动画效果,最好将元素脱离正常的文档流。
避免使用CSS表达式(例如:calc())。
JavaScript
避免频繁操作样式,最好将样式列表定义为class并一次性更改class属性。
避免频繁操作DOM,创建一个documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中。
可以先为元素设置为不可见:display: none,操作结束后再把它显示出来。
5.什么是原型和原型链
原型就是个对象
每个对象都有他自己对应的原型对象,每个对象都可以使用他对象对应的原型对象上面的所有的属性和方法
原型链:查找对象实例的方法和属性时,先在自身找,找不到则沿着__proto__向上查找,我们把__proto__形成的链条关系称原型链
6.数组去重的方式
1、new Set() + Array.from
Set对象:是值的集合,你可以按照插入的顺序迭代它的元素。 Set中的元素只会出现一次,即Set中的元素是唯一的。Set本身是一个构造函数,用来生成 Set 数据结构。类似于数组,不是真正的数组,不能使用 length 方法
Array.from() 方法:对一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。
注意:以上去方式对NaN和undefined类型去重也是有效的,是因为NaN和undefined都可以被存储在Set中, NaN之间被视为相同的值(尽管在js中:NaN !== NaN)
对 {} 无效
const newArr = Array.from(new Set(arr))
console.log(newArr) // [9, 2, '123', true, NaN, false, undefined, {…}, {…}]
2、双层循环,外层循环元素,内层循环时比较值如果有相同的值则跳过,不相同则push进数组
function getDisArray2(arr) {
let newArr = [];
let len = arr.length
for (let i = 0; i < len; i++) {
for (let j = i + 1; j < len; j++) {
if (arr[i] === arr[j]) {
j++
}
}
newArr.push(arr[i]);
}
return newArr
}
getDisArray2(arr) // [9, 2, '123', true, NaN, false, undefined, NaN, {…}, {…}]
3、
利用两层循环+数组的splice方法
function getDisArray3(arr) {
let len = arr.length
for (let i = 0; i < len; i++) {
for (let j = i + 1; j < len; j++) {
if (arr[i] === arr[j]) {
arr.splice(j, 1)
len-- // 减少循环次数提高性能
j-- // 保证j的值自加后不变
}
}
}
return arr
}
getDisArray3(arr) // [9, 2, '123', true, NaN, false, undefined, NaN, {…}, {…}]
4、利用数组的indexOf方法 + forEach
function getDisArray4(arr) {
const newArr = []
arr.forEach(item => {
if (newArr.indexOf(item) === -1) {
newArr.push(item)
}
})
return newArr // 返回一个新数组
}
console.log(getDisArray4(arr))
//[9, 2, '123', true, NaN, false, undefined, NaN, {…}, {…}]
5、利用filter
function getDisArray7(arr) {
return arr.filter(function (item, index, arr) {
return arr.indexOf(item, 0) === index;
});
}
console.log(getDisArray7(arr)) // [9, 2, '123', true, false, undefined, {…}, {…}]
当前元素,在原始数组中的第一个索引==当前索引值,否则返回当前元素
6、利用Map()
function getDisArray4(arr) {
const map = new Map()
const newArr = []
arr.forEach(item => {
if (!map.has(item)) { // has()用于判断map是否包为item的属性值
map.set(item, true) // 使用set()将item设置到map中,并设置其属性值为true
newArr.push(item)
}
})
return newArr
}
getDisArray4(arr) // [9, 2, '123', true, NaN, false, undefined, {…}, {…}]
7.Let、const、var的区别
1.作用域
var声明的变量不存在块级作用域,属于全局作用域。但是存在函数作用域,在函数中用var声明的变量在函数外不能使用,也不能跨函数使用(在其他函数里使用)。
let声明变量的作用域
let声明的变量存在块级作用域,由{ }包裹,不能跨块访问。也存在存在函数作用域,在函数中用let声明的变量在函数外不能使用,也不能跨函数使用(在其他函数里使用)。
const声明变量的作用域
const声明的变量存在块级作用域,由{ }包裹,不能跨块访问。也存在存在函数作用域,在函数中用const声明的变量在函数外不能使用,也不能跨函数使用(在其他函数里使用)。
2.变量提升
let和const声明的变量和函数不存在变量提升,var声明的变量和函数存在变量提升
3.暂时性死区
暂时性死区是指在代码块内,使用let或const命令声明变量之前的区域,该变量在此区域内是不可用的,即属于该变量的“死区”。
因为let,const不存在变量提升所以导致死区的形成,但是var不存在暂时性死区的情况。
在ES6中,暂时性死区的概念是为了防止在变量声明前就使用这个变量,从而导致意料之外的行为。具体来说,当在代码块内尝试访问一个使用let或const声明的变量,但在该声明之前就已经引用了这个变量时,会触发一个ReferenceError错误。可以看如下代码方便理解:
let a = 1
{
//死区开始
console.log(a) //死区:因为存在块级作用域无法访问外部的a,也无法访问到内部的a
//死区结束
let a = 2 //此区域块内创建了a并绑定了值
console.log(a)//此时获取到内部的a
}
4.重复声明
var在同一作用域内,相同的变量可以重复声明变量,let和const不可以
5、全局属性
var关键字声明的变量会挂载到window全局属性上,但是let和const声明的则不会。
6.初始值
1、使用const定义变量时必须赋予其初始值,而var和let则不用
2、const声明的常量必须进行初始化,不允许对常量重新赋值
3、如果const定义的是一个复杂数据类型,可以添加、删除、修改值,但不能改变原有类型
8.Es6新增了哪些新特性
1、let和const
2、symbol
Symbol是ES6中引入的一种新的基本数据类型,用于表示一个独一无二的值,不能与其他数据类型进行运算。它是JavaScript中的第七种数据类型,与undefined、null、Number(数值)、String(字符串)、Boolean(布尔值)、Object(对象)并列。
你可以这样创建一个Symbol值:
const a = Symbol();
console.log(a); //Symbol()
//因为Symbol是基本数据类型,而不是对象,不能 new 。
const a = new Symbol();//报错,Symbol is not a constructor
3、模板字符串
- 在ES6之前,处理模板字符串:
通过“\”和“+”来构建模板 - 对ES6来说:
用${}
来界定;
反引号(``)
直接搞定;
<script>
url="xxxxxx"
// es6之前
let html="<div>"+
" <a>"+url+"</a>"+
"</div>";
//es6
let eshtml=`<div>
<a>${url}</a>
</div>`
</script>
4、解构表达式
结构赋值是对赋值运算符的扩展。它是一种针对数组或者对象进行匹配模式,然后对其中的变量进行赋值。
字符串、以及ES6新增的Map和Set 都可以使用解构表达式
5、函数方面
参数默认值
<script>
function add(a = 0, b = 0) {
return a + b;
}
let x=add();
let y=add(2);
let z=add(3, 4);
console.log(x,y,z); //x=0, y=2, z=7
</script>
5.2 箭头函数
箭头函数实现了一种更加简洁的书写方式。箭头函数内部没有arguments
,也没有prototype
属性,所以不能用new
关键字调用箭头函数。
箭头函数和普通函数最大的区别在于其内部this永远指向其父级对象的this。(重点)
6、运算符
...
扩展运算符
可选链 ?.
函数绑定运算符::
7、模块化
- ES6使用关键字
import
导入模块(文件),有两种常用的方式:
import ‘模块名称’ from ‘路径’;
import ‘路径’;
导出
let name = 'ren',age = 12;
export {name,age};
//注意:变量需要用大括号包裹,然后才能向外输出
模块化优点: 1.防止命名冲突, 2.复用性强
9.普通函数和箭头函数的区别
1. 箭头函数比普通函数更加简洁
如果没有参数,就直接写一个空括号即可
如果只有一个参数,可以省去参数的括号
如果有多个参数,用逗号分割
如果函数体的返回值只有一句,可以省略大括号
2. 箭头函数没有自己的this
箭头函数不会创建自己的this, 所以它没有自己的this,它只会在自己作用域的上一层继承this。所以箭头函数中this的指向在它在定义时已经确定了,之后不会改变。
3. 箭头函数继承来的this指向永远不会改变
4. call()、apply()、bind()等方法不能改变箭头函数中this的指向
5. 箭头函数不能作为构造函数使用
由于箭头函数时没有自己的this,且this指向外层的执行环境,且不能改变指向,所以不能当做构造函数使用。
6. 箭头函数没有自己的arguments
箭头函数没有自己的arguments对象。在箭头函数中访问arguments实际上获得的是它外层函数的arguments值。
7. 箭头函数没有prototype
8. 箭头函数的this指向哪⾥?
箭头函数不同于传统JavaScript中的函数,箭头函数并没有属于⾃⼰的this,它所谓的this是捕获其所在上下⽂的 this 值,作为⾃⼰的 this 值,并且由于没有属于⾃⼰的this,所以是不会被new调⽤的,这个所谓的this也不会被改变。
10.New操作符都做了哪些事情
new 在执行时,会做下面这四件事:
(1)开辟内存空间,在内存中创建一个新的空对象。
(2)让 this 指向这个新的对象。
(3)执行构造函数里面的代码,给这个新对象添加属性和方法。
(4)返回这个新对象(所以构造函数里面不需要 return)。
因为 this 指的是 new 一个 Object 之后的对象实例。于是,下面这段代码:
// 创建一个函数
function Student(name) {
this.name = name; //this指的是构造函数中的对象实例
}