前言
近期整理了JavaScript知识体系,65个知识点由浅入深掌握Js,建议收藏,如有问题,欢迎指正。
1. 说说你对JS的理解
1995年,布莱登·艾奇(美国人)在网景公司,用10天写的一门语言。 Js是一门:动态的,弱类型的,解释型的,基于对象的脚本语言,同时Js又是单线程的。
- 动态类型语言: 代码在执行过程中,才知道这个变量属于的类型。
- 弱类型:声明变量一般用var,数据类型不固定,可以随时改变。可以将字符串'12'和整数3进行连接得到字符串'123',在相加的时候会进行强制类型转换。
- 解释型:一边执行,一边编译,不需要程序在运行之前需要整体先编译。
- 基于对象:最终所有对象都指向
Object。 - 脚本语言 :一般都是可以嵌在其它编程语言当中执行。
- 单线程:依次执行,前面代码执行完后面才执行。
组成部分:
| ECMAscript | DOM | BOM |
|---|---|---|
| JavaScript的语法部分 | 文档对象模型 | 浏览器对象模型 |
| 主要包含JavaScript语言语法 | 主要用来操作页面元素和样式 | 主要用来操作浏览器相关功能 |
2. JS是面向对象语言吗
面向对象:编程思想,对应的是面向过程。面向对象:以类构建对象,对象中有属性和方法。 面向过程:凡事都要自己亲力亲为。
Javascript不是面向对象语言,而是一种基于原型的面向对象的脚本语言。面向对象包括三大特征:继承、封装、多态;而JavaScript中只有封装,JS继承也只是模拟继承,谈不上面向对象。
3. JS数据类型有哪些?值是如何存储的?
Js中一共有8种数据类型:7个基本数据类型和1个对象。
基本数据类型:
- Number
- String
- Boolean
- undefined
- null
- Symbol(ES6新增,表示独一无二的值)
- BigInt(ES6新增,以n结尾,表示超长数据)
对象:
- Object
- function
- Array
- Date
- RegExp
基本数据类型值是不可变的,多次赋值,只取最后一个。
var name = 'DarkHorse';
name.toUpperCase(); // 'DARKHOURSE'
console.log(name); // 'DarkHorse'
基本数据类型存储在栈中,占据空间小、属于被频繁使用数据。 对象值是可变的,可以拥有属性和方法,并且是可以动态改变的。
var a={name:'DarkHorse'};
a.name='xiaohong';
console.log(a.name) //xiaohong
引用数据类型存储在堆中。引用数据类型占据空间大,如果存储在栈中,将会影响程序运行的性能。
引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。
4. Undefined 与 undeclared 的区别?
变量声明未赋值,是 undefined。
未声明的变量,是 undeclared。浏览器会报错a is not defined ,ReferenceError。
5. Null和undefined的区别
null和 undefined 都是基本数据类型,这两个数据类型只有一个值,null和 undefined。
null表示空的,什么都没有,不存在的对象,他的数据类型是object。
初始值赋值为null,表示将要赋值为对象,
不再使用的值设为null,浏览器会自动回收。
undefined表示未定义,常见的为undefined情况:
一是变量声明未赋值,
二是数组声明未赋值;
三是函数执行但没有明确的返回值;
四是获取一个对象上不存在的属性或方法。
6. JS数据类型转换
JS的显式数据类型转换一共有三种:
(1)第一种是:转字符串。有.toString()方法和String()函数,Sting()函数相比于toString()函数适用范围更广,可以将null和undefined转化为字符串,toString()转化会报错。
(2)第二种是:转数值。可以用Number()函数转数值,.parseInt转整数,parseFloat函数转小数。
Number()函数适用于所有类型的转换,比较严格,字符串合法数字则转化成数字,不合法则转化为NAN;空串转化为0,null和undefined转0和NAN;ture转1,false转0。
parseInt()是从左向右获取一个字符串的合法整数位,parseFloat()获取字符串的所有合法小数位。
(3)第三种是:转布尔。像false、0、空串、null、undefined和NaN这6种会转化为false。
常用的隐式类型转换有:任意值+空串转字符串、+a转数值、a-0 转数值等。
var a = 1 + 2 + '3';
// 123
// 任何值和字符串做加法运算,都会先转换为字符串然后进行拼串
var a = 10 - '5'
// 5
// 如果对非数字的值进行算数运算,JS解析器会将值转化为数值再运算
// 总结:字符相连,数值相加(- * /)
7. 数据类型的判断
(1)基本类型的判断——typeof
typeof的返回值有六种,返回值是字符串,不能判断数组和null的数据类型,返回object。
typeof ''; // string 有效
typeof 1; // number 有效
typeof true; //boolean 有效
typeof undefined; //undefined 有效
typeof new Function(); // function 有效
typeof null; //object 无效 这个是一个设计缺陷,造成的
typeof [] ; //object 无效
(2)引用数据类型判断 —— instanceof
检查对象原型链上有没有该构造函数,可以精准判断引用数据类型,不能判断基本数据类型。
[] instanceof Array; //true
{} instanceof Object;//true
new Date() instanceof Date;//true
new RegExp() instanceof RegExp//true
var arr = [1, 2, 3];
console.log(arr instanceof Array) // true
console.log(arr instanceof Object); // true
function fn(){}
console.log(fn instanceof Function)// true
console.log(fn instanceof Object)// true
(3)类似instanceof of —— constructor
每一个对象实例都可以通过 constrcutor 对象来访问它的构造函数。既可以检测基本类型又可以检测对象,但不能检测null和undefined。
console.log((10).constructor===Number);//true
console.log([].constructor===Array);//true
var reg=/^$/;
console.log(reg.constructor===RegExp);//true
console.log(reg.constructor===Object);//false
需要注意的一点是函数的 constructor 是不稳定,如果把函数的原型进行重写,这样检测出来的结果会不准确。
function Fn(){}
Fn.prototype = new Array()
var f = new Fn
console.log(f.constructor)//Array
(4)最准确方式 —— Object.prototype.toString.call()
获取Object原型上的toString方法,让方法执行,让toString方法中的this指向第一个参数的值,最准确方式。
第一个object:当前实例是对象数据类型的(object),
第二个Object:数据类型。
Object.prototype.toString.call('') ; // [object String]
Object.prototype.toString.call(1) ; // [object Number]
Object.prototype.toString.call(true) ; // [object Boolean]
Object.prototype.toString.call(undefined) ; // [object Undefined]
Object.prototype.toString.call(null) ; // [object Null]
Object.prototype.toString.call(new Function()) ; // [object Function]
Object.prototype.toString.call(new Date()) ; // [object Date]
Object.prototype.toString.call([]) ; // [object Array]
Object.prototype.toString.call(new RegExp()) ; // [object RegExp]
Object.prototype.toString.call(new Error()) ; // [object Error]
8. JS中三个基本包装对象
数字、字符串、布尔值作为构造函调用(加new)即为包装对象
包装对象指的是数字、字符串、布尔值分别对应的Number、String、Boolean三个原生对象,这三个原生对象可以把原始对象的值包装成对象。
var v1 = new Number(123);
var v2 = new String('abc');
var v3 = new Boolean(true);
typeof v1 // "object"
typeof v2 // "object"
typeof v3 // "object"
9. a++ 和 ++a 区别
无论是前++还是后++,都会使原来变量立刻自增1。 不同在于a++是原值,++a是新值。
var a = 4;
console.log(a++); //4 原值
var b = 4;
console.log(++b) //5 新值
10. 0.1+0.2 === 0.3吗
在开发过程中遇到类似这样的问题:
let n1 = 0.1, n2 = 0.2
console.log(n1 + n2) // 0.30000000000000004
显然,这里得到的不是想要的结果。
计算机是使用二进制存储数据,整数转换没有误差,如9转换成二进制是1001,而小数的话用二进制转化不准确,比如0.2会转化成0.1100…,所有用二进制表示数据的计算机编程语言都是这样的。
使用.toFixed(),四舍五入
(n1 + n2).toFixed(2)
经常操作小数要求精度,使用第三发库:Math.js
11. JS的作用域和作用域链
作用域就变量起作用的范围和区域。 作用域的目的是隔离变量,保证不同作用域下同名变量不会冲突。
JS中,作用域分为三种,全局作用域、函数作用域和块级作用域。 全局作用域在script标签对中,无论在哪都能访问到。在函数内部定义的变量,拥有函数作用域。块级作用域则是使用let和const声明的变量,如果被一个大括号括住,那么这个大括号括住的变量区域就形成了一个块级作用域。
作用域层层嵌套,形成的关系叫做作用域链,作用域链也就是查找变量的过程。 查找变量的过程:当前作用域 --》上一级作用域 --》上一级作用域 .... --》直到找到全局作用域 --》还没有,则会报错。
作用域链是用来保证——变量和函数在执行环境中有序访问。
12. LHS和RHS查询
LHS和RHS查询是JS引擎查找变量的两种方式,这里的“Left”和“Right”,是相对于赋值操作来说,当变量出现在赋值操作左侧时,执行LHS操作。
LHS 意味着 变量赋值或写入内存,,他强调是写入这个动作。
var name = '小明'
当变量出现在赋值操作右侧或没有赋值操作时,是RHS。
var Myname = name
console.log(name)
RHS意味着 变量查找或读取内存,它强调的是读这个动作。
13. 词法作用域和动态作用域
Js底层遵循的是词法作用域,从语言的层面来说,作用域模型分两种:
词法作用域:也称静态作用域,是最为普遍的一种作用域模型
动态作用域:相对“冷门”,bash脚本、Perl等语言采纳的是动态作用域
词法作用域:在代码书写时完成划分,作用域沿着它定义的位置往外延伸。 动态作用域:在代码运行时完成划分,作用域链沿着他的调用栈往外延伸。
更多详细的内容可看我之前文章:《深入理解JS中的词法作用域与作用域链》
14. 什么是匿名函数,有什么作用
匿名函数也叫一次性函数,没用名字,且在定义时执行,且执行一次,不存在预解析(函数内部执行的时候会发生)。
匿名函数的基本形式为:
(function(){
...
}());
除此之外,还有常见的以下写法
(function(){
console.log('我是一个匿名函数')
})();
var a = function(){
console.log('我是一个匿名函数')
}()
// 使用多种运算符开头,一般是用!
!function(){
console.log('我是一个匿名函数')
}();
匿名函数的作用有:
(1)对项目的初始化,页面加载时调用,保证页面有效的写入Js,不会造成全局变量污染 (2)防止外部命名空间污染 (3)隐藏内部代码暴露接口
15. 什么是回调函数,常见的回调函数有哪些
回调函数是一段可执行的代码段,它作为一个参数传递给其他的代码,其作用是在需要的时候方便调用这段(回调函数)。
在Js中函数也是对象的一种,同样对象可以作为参数传递给函数,因此函数也可以作为参数传递给另一个函数,这个做为参数的函数就是回调函数。
简单来说:回调函数即别人调用了这个函数,即函数作为参数传入另一个函数。
一般来说回调函数满足三个条件:自己定义的函数、自己没有调用,函数最终执行了。
在开发中经常看到的回调函数有:
// 点击事件的回调函数
$('#btn').click(function(){
console.log('click btn');
})
// 异步请求的回调函数
$.get('ajax/test.html',function(data){
$('#box').html(data);
})
// 计时器
var timeId = setTimeout(function{
console.log('hello')
},1000)
16. 什么是构造函数,与普通函数的区别
在ES6之前,我们都是通过构造函数创建类,从而生成对象实例。 构造函数就是一个函数,只不过通常我们把构造函数的名字写成大驼峰, 构造函数和普通函数的区别,构造函数通过new关键字进行调用,而普通函数直接调用。
// 创建一个类(函数)
function Person(name,age){
this.name = name;
this.age = age;
this.eat = function(){
console.log('我爱吃');
}
}
// 普通函数调用
var result = Person('张三',18);
console.log(result);
// 构造函数调用
var p1 = new Person('李四',16);
console.log(p1);
var c2 = new Person('王五',14);
console.log(p2);
17. 函数中arguments 的对象是什么
函数在调用时JS引擎会向函数中传递两个的隐含参数,一个是this(后面我们会说到),另一个就是arguments,arguments是一个伪数组,主要作用是:获取函数中在调用时传入的实参。
function add(){
console.log(arguments);
return arguments[0] + arguments[1];
}
add(10,20);
使用arguments.length可以获取传递实参的个数,同时也可以让我们的函数具有多重功能。
function addOrSub(a,b,c){
if(arguments.length == 2){
return a - b;
}else if(arguments.length == 3){
return a + b + c;
}
}
console.log(addOrSub(10,20));//传递两个实参就做减法
console.log(addOrSub(10,20,30));//传递的是三个实参就做加法
18. 列举常用字符串方法
| 方法名 | 功能 | 原字符串是否改变 |
|---|---|---|
| charAt() | 返回指定索引的字符 | n |
| charCodeAt(0) | 返回指定索引的字符编码 | n |
| concat() | 将原字符串和指定字符串拼接,不指定相当于复制一个字符串 | n |
| String.fromCharCode() | 返回指定编码的字符 | n |
| indexOf() | 查询并返回指定子串的索引,不存在返回-1 | n |
| lastIndexOf() | 反向查询并返回指定子串的索引,不存在返回-1 | n |
| localeCompare() | 比较原串和指定字符串:原串大返回1,原串小返回-1,相等返回0 | n |
| slice() | 截取指定位置的字符串,并返回。包含起始位置但是不包含结束位置,位置可以是负数 | n |
| substr() | 截取指定起始位置固定长度的字符串 | n |
| substring() | 截取指定位置的字符串,类似slice。起始位置和结束位置可以互换并且不能是负数 | n |
| split() | 将字符串切割转化为数组返回 | n |
| toLowerCase() | 将字符串转化为小写 | n |
| toUpperCase() | 将字符串转化为大写 | n |
| valueOf() | 返回字符串包装对象的原始值 | n |
| toString() | 直接转为字符串并返回 | n |
| includes() | 判断是否包含指定的字符串 | n |
| startsWith() | 判断是否以指定字符串开头 | n |
| endsWith() | 判断是否以指定字符串结尾 | n |
| repeat() | 重复指定次数 | n |
19. 列举常用数组方法
| 方法名 | 功能 | 原数组是否改变 |
|---|---|---|
| concat() | 合并数组,并返回合并之后的数据 | n |
| join() | 使用分隔符,将数组转为字符串并返回 | n |
| pop() | 删除最后一位,并返回删除的数据,在原数组 | y |
| shift() | 删除第一位,并返回删除的数据,在原数组 | y |
| unshift() | 在第一位新增一或多个数据,返回长度,在原数组 | y |
| push() | 在最后一位新增一或多个数据,返回长度 | y |
| reverse() | 反转数组,返回结果,在原数组 | y |
| slice() | 截取指定位置的数组,并返回 | n |
| sort() | 排序(字符规则),返回结果,在原数组 | y |
| splice() | 删除指定位置,并替换,返回删除的数据 | y |
| toString() | 直接转为字符串,并返回 | n |
| valueOf() | 返回数组对象的原始值 | n |
| indexOf() | 查询并返回数据的索引 | n |
| lastIndexOf() | 反向查询并返回数据的索引 | n |
| forEach() | 参数为回调函数,会遍历数组所有的项,回调函数接受三个参数,分别为value,index,self;forEach没有返回值 | n |
| map() | 同forEach,同时回调函数返回数据,组成新数组由map返回 | n |
| filter() | 同forEach,同时回调函数返回布尔值,为true的数据组成新数组由filter返回 | n |
| Array.from() | 将伪数组对象或可遍历对象转换为真数组 | n |
| Array.of() | 将一系列值转换成数组 | n |
| find | 找出第一个满足条件返回true的元素 | n |
| findIndex | 找出第一个满足条件返回true的元素下标 | n |
注意:重点关注方法的:功能、参数、返回值
20. 如何判断一个对象是空对象
方法一:将对象转换成JSON字符串,再判断是否等于“ {} ”
let obj={};
console.log(JSON.stringify(obj)==="{}");
//返回true
方法二:Object.keys()方法,返回对象的属性名组成的一个数组,若长度为0,则为空对象
console.log(Object.keys(obj).length==0);
//返回true
方法三:for in循环
let result=function(obj){
for(let key in obj){
return false;//若不为空,可遍历,返回false
}
return true;
}
console.log(result(obj)); //返回true
21. 什么是DOM和BOM
DOM:文档对象模型,将文档看做是一个对象,这个对象主要定义了处理网页内容的方法和接口,通过JS操作页面元素。
BOM:浏览器对象模型,将浏览器看做是一个对象,定义了与浏览器进行交互的方法和接口,通过JS操作浏览器。
BOM的核心是window,window 对象子有 location navigator history。
DOM的最根本的document对象也是window 对象的子对象。
window对象
window对象是BOM的顶级对象,称作浏览器窗口对象- 全局变量会成为
window对象的属性 - 全局函数会成为
window对象的方法
- window.onload
- window.onresize
- window.onscroll
Location对象
- 提供了url相关的属性和方法。一些常用的有:
// url相关属性
location.href
// 返回当前加载页面的完整URL
location.protocal
// 返回页面使用的协议
location.search
// 返回URL的查询字符串,查询?开头的的字符串
location.reload();
// reload():实现的是页面刷新
location.assign("https://www.baidu.com");
// assign():可以打开新的页面,并且可以返回,可以产生历史纪录的
location.replace("https://www.baidu.com");
// replace():用新文档替换当前的文档,但不能返回,没有产生历史记录
history对象
- 提供了对象包含浏览器的历史记录, 这些历史记录以栈的形式保存。页面前进则入栈,页面返回则出栈。
history.back();//历史记录返回上一页
history.forward();//去到下一页
history.go(-2);//去到指定的历史记录页 0代表当前页 -1代表之前 1代表之后
navigator对象
- 提供了浏览器相关的信息,比如浏览器的名称、版本、语言、系统平台等信息。
console.log(window.navigator.appName);//Netscape
console.log(window.navigator.appVersion);//浏览器版本
console.log(window.navigator.appCodeName);//浏览器内核版本,但是打印出来一般
screen对象
- 提供了用户显示屏幕的相关属性,比如显示屏幕的宽度、高度。
console.log(window.screen.width);//屏幕的宽 分辨率
console.log(window.screen.height);//屏幕的高
22. DOM树简单描述一下
以Html为根节点,形成的一棵倒立的树状结构,我们称作DOM树。这个树上所有的东西都叫节点,节点有很多类(元素、属性、文本),通过DOM方法去获取或者去操作节点,就叫DOM对象。
Document对象
指这份文件,也就是这份 HTML 档的开端。当浏览器载入 HTML 文档, 它就会成为 Document 对象。
重绘:DOM元素的样式发生改变,浏览器会重新渲染这个元素。
回流:DOM元素结构或者位置发生改变(删除、增加、改变位置大小),浏览器重新计算渲染整个DOM树。
23. DOM操作有哪些
(1)查找节点
- getElementById //按照 id 查询
- getElementsByTagName //按照标签名查询
- getElementsByClassName //按照类名查询
- querySelectorAll //按照css 选择器查询
(2)创建节点
document.write()
innerHTML
createElement()和appendChild()
(3)添加、移除、替换、插入
appendChild(node); //插入节点
removeChild(node); //移除节点
replaceChild(new,old); //替换节点
insertBefore(new,old) //追加节点
(4)属性操作
getAttribute(key); //获取自定义属性
setAttribute(key, value); //设置自定义属性
hasAttribute(key); //是否存在该属性
removeAttribute(key); //移除属性
(5)内容修改
InnerText(); // 无标签效果
InnerHTML(); // 有标签效果
Text-content // IE9以上支持,类似innerText
24. 什么是事件传播
当事件发生在DOM对象上,该事件并不完全发生在这个元素上。
关于事件传播,IE和网景公司有不同的理解,IE认为,事件应该是冒泡阶段,网景公司认为事件应该是捕获阶段,随后W3C综合了两个公司的方案,JS同时支持冒泡和捕获流,并以此确定事件流标准。这个标准也叫DOM2事件流。
事件传播的三个阶段:
(1)事件捕获
事件从window开始,从外向内,直到到达目标事件或event.target
(1)目标阶段
事件到达目标元素,触发监听事件
(2)事件冒泡
事件从目标元素开始冒泡,从内向外,直到到达window。
当事件被触发时,首先经历的是一个捕获过程,事件会从最外层元素开始“穿梭”,逐层“穿梭”到最内层元素。这个穿梭过程会持续到事件抵达他目标的元素(也就是真正触发这个事件的元素)为止。此时事件流接切换到了“目标阶段”——事件被目标元素所接收然后事件会会弹,进入到冒泡阶段——他会沿着来时的路“逆流而上”,一层一层再走回去。
也就是说当事件在层层DOM元素穿梭时,所到之处都会触发事件处理函数。
25. 三种事件模型是什么
DOM0级事件模型
通过对象.onclick形式绑定,同一元素绑定多个相同事件,后会覆盖前边,事件不会传播,不存在事件流概念。
<body>
<div id="box"></div>
<button>解绑</button>
<script>
window.onload = function () {
var box = document.querySelector('#box');
var btn = document.querySelector('button');
//dom0 绑定
box.onclick = function () {
console.log('我是dom0级事件1')
};//我是dom0级事件1
box.onclick = function () {
console.log('我是dom0级事件2')
};//我是dom0级事件
}
解绑 事件类型 = null
btn.onclick = function () {
box.onclick = null;
}
DOM2级事件模型
通过addEventListener绑定,三个参数,不带on的事件类型,回调函数,事件阶段,默认是false,冒泡阶段。可以绑定多个相同事件,事件从上到下执行。this指向当前绑定事件对象。
box.addEventListener("click",function(){
console.log('今天中午吃多了')
},false)
box.addEventListener('click',fun,false);
function fun() {
console.log('晚上就不吃了')
}
通过removeEventListener解绑,解绑参数与绑定参数一致,且事件需要解绑,那么函数必须定义成有名函数。
box.onclick=function(){
box.removeEventListener(fun)
}
IE事件模型(低级浏览器)
通过attachEvent绑定,两个参数,带on的事件类型,回调函数。this指向window。
box.attachEvent("onclick",function(){
console.log("今天晚上又吃了")
})
box.attachEvent("onclick",fun);
function fun(){
console.log("伤心")
}
通过detachEvent解绑,解绑参数与绑定参数一致。
btn.onclick = function(){
box.detachEvent("onclick",fun)
}
26. 什么是冒泡阶段注册监听而不是不是捕获阶段
捕获是计算机处理事件的逻辑,冒泡是人处理事件的逻辑。
举个例子,点击鼠标获取位置信息,操作系统会根据位移计算出位置信息,提供给浏览器,把具体坐标转化到具体元素上事件过程,就是捕获。
而冒泡过程,是符合人类理解逻辑的:我按到了电视机开关时,我就按到了电视机。
27. 事件对象有哪些常用属性
当DOM接受了一个事件,对应的事件处理函数被触发时,就会产生一个事件对象event作为事件处理函数的入参。这个对象中包含着与事件有关的信息,比如事件是由哪个元素触发的,事件类型等。常用属性有:
- target
事件绑定的元素
- currentTarget
触发事件的元素,两者没有冒泡的情况下,是一样的值,但在使用了事件委托的情况下,就不一样了。
preventDafault
阻止默认行为,比如阻止超链接跳转、在form中按回车会自动提交表单。
e.preventDefault();
stopPropagation
阻止事件冒泡,将事件处理函数的影响控制在目标元素范围内
e.stopPropagation();
阻止事件冒泡,需要注意一点的是:谷歌火狐的组织行为是:event.stopPropagation(),而IE:event.cancelBubble=true
28. 什么是事件委托
原理:
如果子元素有很多,且子元素的事件监听逻辑都相同,将事件监听绑定到父元素身上或者共有的祖先元素上 。事件委托原理是利用事件冒泡,子元素触发,父元素执行回调函数。
好处:
(1)减少事件的绑定次数
(2)新增元素不需要单独绑定
应用:
页面上有多个li,点击每一个元素,都输出他的文本内容。
<body>
<ul id="poem">
<li>鹅鹅鹅</li>
<li>曲项向天歌</li>
<li>白毛浮绿水</li>
<li>红掌拨清波</li>
<li>锄禾日当午</li>
<li>汗滴禾下土</li>
<li>谁知盘中餐</li>
<li>粒粒皆辛苦</li>
<li>背不动了</li>
<li>我背不动了</li>
</ul>
</body>
一个直观的思路是让每一个li元素都去监听一个点击动作,但是这样子并不好,我们可以采用事件委托的方式实现。
var ul = document.getElementById('poem')
ul.addEventListener('click', function(e){
console.log(e.target.innerHTML)
})
点击任何一个li,点击事件都会被冒泡到li共同的Ul上,我们通过Ul感知到这个冒泡来的事件,在通过e.target 拿到实际触发事件的那个元素,通过事件委托只执行一次DOM操作,减少了内存开销,大大提升了开发效率。
29. ECMAScript 是什么
ES是JS的标准,约束条件。广义的JS=ES+DOM+BOM,狭义的JS就是ES。EC是为了保证JS在浏览器运行结果一致。
是由欧洲计算机协会(ECMA)这个组织制定的。这个组织的目标是制定和发布脚本语言规范。组织会定期定期召开会议,会议由一些公司的代表与特邀专家出席。
30. ECMAScript 2015(ES6)有哪些新特性?
在2011年ECMA组织就开始着手制作第6个版本规范,由于这个版本引入的功能语法太多,最终标准的制作者决定在每年6月份发布一次,版本号为年号代替,ES6正式发布于2015 年 6 月。我们现在所说的ES6是一个泛指,泛指ES2015之后的版本。
- 块级作用域
- 对象数组解构赋值
- 模板字符串
- 箭头函数
- 延展运算符
- 剩余参数
- 声明类
- set、map集合
- Promise
31. Var,Let和Const的区别是什么
let 关键字用来声明变量,使用let声明的变量有以下特点:
不允许重复声明
let name = '张三'
let name = '李四'
console.log(name);
// SyntaxError
let num = 1;
num = 2;
console.log(num);//2
// 可以重复赋值
不存在预解析
预解析:JS引擎在JS代码正式执行之前会做一些预解析的工作。
- 先把
var变量声明提前 - 再把以
function开头的整个函数提前
console.log(num)
//undefined
var num = 10
console.log(num)
let num = 10
// ReferenceError
具有块级作用域
块级作用域:使用let声明变量,如果被一个大括号括住,那么这个大括号括住的变量就形成了一个块级作用域。
块级作用域定义的变量只在当前块中生效,这和函数作用域类似。
{
let num = 10;
console.log(num);
}
console.log(num); // 报错
ES6中规定,let/const命令会使区块形成封闭作用域,在声明之前使用变量,就会报错。这在语法上,称为“暂时性死区”
块级作用域还有的一个好处:防止循环变量变成全局变量
for(var i=0;i<2;i++){
}
console.log(i);
//2
for(let i=0;i<2;i++){
}
console.log(i);
//i is not defined
const 关键字
和let类似,不可重复声明,不存在预解析,拥有块级作用域。同时使用const声明的变量值无法改变,常用于声明常量,常量名一般为大写,单词间用下划线。
const PI = 3.14
PI = 100
// Missing initializer in const declaration
必须要有初始值
const PI
console.log(PI)
// Missing initializer in const declaration
可以修改数组和对象元素
const obj = {}
obj.age = 10
console.log(obj.age)10
const arr = []
arr.push(10)
console.log(arr)//[10]
32. const对象的属性可以修改吗
const保证的并不是变量的值不能改动,而是指向那个内存地址不能改动,对于基本数据类型(数值、字符串、布尔值),其值就保存在变量指向的那个内存地址,因此等同于常量。
但对于引用数据类型(主要是对象和数组),变量指向数据的内存地址,保存的只是一个指针,const只能保证这个指针是固定不变的,至于他指向的数据结构是不是可变的,就不完全能控制了。
33. 什么是解构赋值
在es6之前,获取对象或者数组中数据,只能通过属性访问的形式并赋值给本地变量,这样需要写许多相似的代码。
const obj = {
name: '张三',
age: 20,
}
const name = obj.name
const age = obj.age
console.log(name,age)// 张三 20
有了解构赋值可以方便获取数组中的数据,获取对象中属性和方法。 我们可以直接从数组或对对象中提取数据,并赋值给变量。
数组解构赋值
let arr=[1,2,3];
// 定义变量并接收
let [s1,s2,s3]=arr;// 完全解构
let [s1,,s3]=arr; // 不完全解构
console.log(s1);// 1
对象的解构赋值
const obj = {
name: '张三',
age,
sayHi: function () {
console.log('你好')
}
}
// 定义变量,对应属性,想要什么属性就写什么属性
const { name, sayHi } = obj
console.log(name);// 张三
sayHi();// 你好
// 为属性取别名
const {name:defaultName,sayhi} = obj;
console.log(defaultName);
// 设置属性默认值
const {age=20} = obj;
console.log(age);//20
34. 什么是模板字符串
模板字符串是增强版的字符串,用反引号 表示,模板字符串可以当普通字符串使用,也可以用来定义多行字符串,作用是简化字符串的拼接。特点如下:
可以出现换行符
// 可以出现换行符
document.write(`
<button>1</button>
<button>2</button>
<button>3</button>
<button>4</button>
<button>5</button>
`)
可以输出变量
// 通过 ${} 形式输出变量
const age = 100
const text = `今年过年,小明的年龄已经是:${age}`;
console.log(text)
35. 箭头函数和普通函数区别
ES6中允许使用箭头 => 定义函数,主要作用不仅仅是:简化function写法,更重要的是改变this指向。
基本用法
// es6以前写法
const add =function(a,b){
return a + b
}
const result = add(10,20)
console.log(result) //30
// es6写法
const add =(a,b)=>{
return a + b
}
const result = add(10,20)
console.log(result) //30
与普通函数区别
- 不能作为构造函数实例化
const Person =()=>{
name:'小明'
};
var per = new Person();
console.log(per.name)
// Person is not a constructor
- 不能使用 arguments
const f4 =(arguments)=>{
console.log(arguments.length)
}
f4(1,2,3,4,5)
// undefined
- 箭头函数的this是不能改变
window.name = '小明'
const f5 = () => {
console.log(this.name) // 小明
};
const f6 = function () {
console.log(this.name) // 小明
};
f5();
f6();
var obj = {
name: '大明'
}
f5.call(obj) // 小明
f6.call(obj) // 大明
- this指向包裹箭头函数的第一个普通函数
let school = {
name: '小明',
getName(){
let fn7 = () => {
console.log(this); // 小明
}
fn7();
}
};
36. 什么是剩余运算符
剩余运算符中最重要的特点就是代替以前的arguments,利用剩余运算符可以获取函数调用时传递的参数,并且返回值是一个真数组。
function f1(...args) {
console.log(args);
// [1,2,3]
}
f2(1, 2, 3)
// 形参较多,放在最后位置
function f3(a, b, ...args) {
console.log(a,b);//1,2
console.log(args);// [3,4,5]
//
}
f3(1, 2, 3, 4, 5)
37. 延展运算符使用过吗
拆包和打包数组、对象
function f2(...args) {
console.log(args)
}
f2(1, 2, 3, 4, 5);
// [1, 2, 3, 4, 5]
function f3(...args) {
console.log(...args)
}
f3(1, 2, 3, 4, 5);
// 1 2 3 4 5
数组合并
var arr1=[10,20,30];
var arr2=[40,50,60];
var arr=[...arr1,...arr2];
console.log(arr);
// [10, 20, 30, 40, 50, 60]
对象合并
字面量复制对象 let obj={ } {…obj}
var obj1={
name:'自来也',
age:45
}
var obj2={
gender:'男',
hobby(){
console.log(console.log('吃饭'))
}
}
var obj={
name:'菲儿',
...obj1,
...obj2
}
console.log(obj);
// {name: "自来也", age: 45, gender: "男", hobby: ƒ}
数组的克隆
const arr3 = [10, 20, 30]
const arr4 = [...arr3]
console.log(arr4)
// [10, 20, 30]
伪数组转真数组
const arr5 = document.getElementsByTagName('button');
console.log(arr5);
//[button, button, button]
console.log(arr5 instanceof Array);//false;
console.log([...arr5] instanceof Array);//true
console.log(arr5);
38. ES6中类了解多少
类(class)是ES6中语法糖,最终还是转化成构造函数去执行,使用class创建的类会将方法自动加到原型上。
class Person{
// 通过构造函数 -- 初始化实例化对象属性
constructor(name,age){
this.name=name;
this.age= age;
}
// 添加方法 不需要添加,
eat(){
console.log('哈哈')
}
}
const per = new Person('小明',20);
console.log(per.name);
per.eat();
class实现继承
- 通过关键字extends*实现继承
- 子类必须在constructor方法中调用super*方法
class Child extends Person{
constructor(name) {
super(name);
this.childName = 'child';
}
childMethod() {
console.log('childMethod');
}
}
39. set集合和map集合了解多少
set
- 是一个构造函数,用来存储任意数据类型的唯一值;
- 可以存储数组、字符串,返回值是一个对象。
定义Set集合
// 定义set集合
const s1 = new Set()
console.log(s1)
// { }
//打印长度
console.log(s1.size);// 0
传入数据
const s = new Set([10,20,30,40,40]);
console.log(s);
// {10, 20, 30, 40}
// 打印出来是一个集合 需要拆包
console.log(...s);
// 10 20 30 40
set方法
// 添加数据
const s = new Set();
// 向Set集合中添加一个数据
s.add('a').add('b');
console.log(...s);
// a b
// 移除数据
r1 = s.delete('a');
console.log(r1);
// 返回结果是ture值,代表删除成功
// 是否存在这个数据
r2 = s.has(9);
console.log(r2);//false
// 清空数据
r3 = s.clear()
console.log(r2);// undefined
应用
- 数组去重
let arr1=[1,2,3,4,4,5,1];
// {1, 2, 3, 4, 5} 1,2,3,4,5 []
let arr2=[...new Set(arr1)];
console.log(arr2);
// [1, 2, 3, 4, 5]
- 交集操作
const arr3 = [1, 2, 3, 4, 5, 6, 7];
const arr4 = [1, 2, 3, 10, 11];
//对数组进行拆包 过滤 判断4里面是否存在item
const result = [...new Set(arr3)].filter(item => new Set(arr4).has(item));
console.log(result);
- 并集操作
const arr5 = [1, 2, 3, 4, 5];
const arr6 = [1, 2, 3, 6, 7, 8];
const result = [...new Set([...arr5,...arr6])]
console.log(result);
- 差集操作(我有的你没有,或者你有的我没有)
const arr7 = [1, 2, 3, 4, 5];
const arr8 = [1, 2, 3, 8, 9];
// 判断arr8中是否含有数组中每一项数据
const result=[...new Set(arr7)].filter(!(item=>new Set(arr8).has(item)));
const result=[...new Set(arr8)].filter()
map集合
类似于对象,存放键值对,键和值可以是任何数据类型。
- 键值的方式添加数据
var m = new Map();
map.set('name', '强哥')
map.set(obj, function(){console.log('真好')})
- 读取 删除 判断 清空
// 根据键获取值
console.log(map.get(obj))
// 根据键进行删除
map.delete('name')
// 根据键进行判断
console.log(map.has('name'))
// 清空map
map.clear()
40 Map和weakMap区别
- weakMap和Map结构类似,但weakMap只接受对象作为键名
- weakMap的键名指向的对象,不计入垃圾回收机制
41. Proxy 可以实现什么功能
在 Vue3.0 中通过 Proxy 来替换原本的 Object.defineProperty 来实现数据响应式。
Proxy 是 ES6 中新增的功能,它可以用来自定义对象中的操作。
let p = new Proxy(target, handler)
target 代表需要添加代理的对象,handler 用来自定义对象中的操作,比如可以用来自定义 set 或者 get 函数。
下面来通过 Proxy 来实现一个数据响应式:
let onWatch = (obj, setBind, getLogger) => {
let handler = {
get(target, property, receiver) {
getLogger(target, property)
return Reflect.get(target, property, receiver)
},
set(target, property, value, receiver) {
setBind(value, property)
return Reflect.set(target, property, value)
}
}
return new Proxy(obj, handler)
}
let obj = { a: 1 }
let p = onWatch(
obj,
(v, property) => {
console.log(`监听到属性${property}改变为${v}`)
},
(target, property) => {
console.log(`'${property}' = ${target[property]}`)
}
)
p.a = 2 // 监听到属性a改变
p.a // 'a' = 2
在上述代码中,通过自定义 set 和 get 函数的方式,在原本的逻辑中插入了我们的函数逻辑,实现了在对对象任何属性进行读写时发出通知。
当然这是简单版的响应式实现,如果需要实现一个 Vue 中的响应式,需要在 get 中收集依赖,在 set 派发更新,之所以 Vue3.0 要使用 Proxy 替换原本的 API 原因在于 Proxy 无需一层层递归为每个属性添加代理,一次即可完成以上操作,性能上更好,并且原本的实现有一些数据更新不能监听到,但是 Proxy 可以完美监听到任何方式的数据改变,唯一缺陷就是浏览器的兼容性不好。
43 模块化规范有哪些
浏览器模块化规范
- AMD:Require.js
- CMD:Sea.js
服务器端模块化:
- Node 中的Common.js
- 导入模块:Require 导出模块:Modules.exports
浏览器与服务器通用的开发规范——ES6模块化
- 一个JS文件就是一个模块
- 导入模块import 导出模块 export
44 CommonJS与ES6模块区别
- 语法不同:
Common:require+modules.exports ES6:import+export
- this指向
而ESModule中顶层this指向undefined,Common的this指向这个模块本身
- ES6可以实现Tree-shaking
- ES6模块化静态引入编译时引用(不能通过条件判断是否引用)
- Common.js是动态引入,执行时引用。只有静态分析,才能实现Tree-shaking
Tree-shaking
Tree树,shaking摇晃的意思,就是理解为把那些没用的树叶子摇下来。
比如说我们引用了一个模块,这个模块中有好几个方法,我们只用了一个方法,我们没有用到的函数就给删掉了,打包的时候不会加载到。好处就是代码体积会更小一些,加载更快一些。
- 循环依赖处理不同
- ES6 在编译时会进行循环依赖处理
- 而 CommonJS 则无法处理循环依赖
- 深拷贝与浅拷贝
- CommonJS 是对模块的浅拷贝
- ES6是对模块的引用。如果在 ES6 的模块中修改一个导出变量的属性,那么其他导入该变量的模块也会受到影响
45. promise的了解
产生
- Es6中异步编程新解决方案,主要解决异步回调地域问题。
回调地域:函数嵌套或函数很乱的调用,回调地狱不利于阅读,不能直接return,不能直接捕获。
setTimeout(() => {
console.log(1)
setTimeout(() => {
console.log(2)
setTimeout(() => {
console.log(3)
},3000)
},2000)
},1000)
- 本意是承诺许诺的意思
- 从功能的角度来说,promise对象包裹一个异步操作,并获取成功或失败的返回值。
- 从语法的角度来说,promise对象是一个构造函数,用来生成promise实例。
两个参数
- 异步操作成功调用resolve,会触发than回调函数,可以传参。
- 异步操作失败调用reject,会触发catch回调函数,可以传参。
三种状态
- 初始状态pending
- 操作成功resolved
- 操作失败rejected
resolve,reject 都没有执行,pending状态
const p1 =new Promise((resolve,reject)=>{
})
console.log(p1) // pending 状态
resolve调用,fulfielled状态
const p2 =new Promise((resolve,reject)=>{
setTimeout(() => {
resolve()
});
})
// 一开始打印时是pedding
console.log(p2) // promise{<pending>}
// 执行完setTimeout 打印为resolved
setTimeout(()=>{
console.log(p2) // Promise{<fulfilled>}
})
rejected调用了,rejected状态
const p3 =new Promise((resolve,reject)=>{
setTimeout(() => {
reject()
});
})
// 一开始打印时是pedding
console.log(p3) // Promise{<pending>}
// 执行完setTimeout 打印为reject
setTimeout(()=>{
console.log(p3) // Promise {<rejected>:undefined}
})
promise对象状态一旦确定下来,不可改变
let p = new Promise((resolve,reject) =>{
// 调用完成功在调用失败,输出的还是成功状态
resolve(1);
reject(2)
})
p.then(res=>{
console.log(res) // 1
},err=>{
console.log(err)
})
46. Promise状态改变
- 状态的改变是通过 resolve() 和 reject() 函数来实现的
- 异步操作成功调用resolve,会触发than回调函数,可以传参。
- 异步操作失败调用reject,会触发catch回调函数,可以传参
resolve状态
const p1 = Promise.resolve(100)
p1.then(data=>{
console.log('data',data) // data 100
}).catch(err=>{
// resolve状态,不会触发catch回调
console.log('err',err)
})
rejected状态
const p2 = Promise.reject(100)
p2.then(data=>{
// reject状态,不会触发then回调
console.log('data',data)
}).catch(err=>{
console.log('err',err) //err 100
})
then正常返回resolved,里面有报错返回rejected
const p1 = Promise.resolve().then(()=>{
return 100
})
console.log(p1) // resolve成功状态
// resolve触发后续then的回调
p1.then(()=>{
console.log('123')
})
// then里有报错返回rejected
const p2 = Promise.resolve().then(()=>{
throw new Error('then err')
})
p2.then(()=>{
console.log(456)
}).catch(err=>{
console.log(err) //then err
})
catch正常返回resolved,里面有报错返回rejected
const p3 = Promise.reject('my error').catch(err =>{
console.error(err)
})
console.log(p3) // resolved 触发then
// 虽然是catch触发,但是catch后面还可以.then
p3.then(()=>{
console.log(100) // 100
})
const p4 = Promise.reject('my error').catch(err=>{
throw new Error ('catch err')
})
console.log(p4) // rejected 触发catch回调
p4.then
e.log(100)
}).catch(()=>[
console.log('some err') // some err
])
// 问题:p4这一段返回什么状态
// resolve状态promise,通过catch返回的promise,catch没有报错
47 .Promise静态方法
- than
指定成功的回调
- catch
指定失败的回调
function foo(flag){
if(flag){
return new Promise(resolve=>{
resolve('成功')
})
}else{
return Promise.reject('失败')
}
}
foo(false).then(res=>{
console.log(res)
},err=>{
console.log(err) // 失败
})
- all
Promise.all([promise1,promise2,promise3]) 批量一次性发送多个异步请求 返回值也是promise 如果数组中的promise都是成功的,那么返回的这个promise就是成功的,成功的结果就是所有的promise的成功结果组成的数组;
let p1 = new Promise((resolve)=>{
setTimeout(()=>{
console.log(1)
resolve('1成功')
},1000)
})
let p2 = new Promise((resolve)=>{
setTimeout(()=>{
console.log(2)
resolve('2成功')
},2000)
})
let p3 = new Promise((resolve)=>{
setTimeout(()=>{
console.log(3)
resolve('3成功')
},3000)
})
Promise.all([p1,p2,p3]).then(res=>{
console.log(res)
})
// 1 2 3
// (3) ['1成功', '2成功', '3成功']
如果数组中的promise有一个是失败的,那么返回的这个promise就是失败的,失败的原因就是那个失败的promise的失败原因;
let p1 = new Promise((resolve)=>{
setTimeout(()=>{
console.log(1)
resolve('1成功')
},1000)
})
let p2 = new Promise((resolve,reject)=>{
setTimeout(()=>{
console.log(2)
reject('2失败')
},2000)
})
let p3 = new Promise((resolve)=>{
setTimeout(()=>{
console.log(3)
resolve('3成功')
},3000)
})
Promise.all([p1,p2,p3]).then(res=>{
console.log(res)
},err=>{
console.log(err) // 2失败
})
// 1 2 2失败 3
- race
多个promise任务同步执行,返回最先结束的promise任务结束,不论是成功还是失败,简单来说就先到先得。
48 promise应用场景
promise.all 应用:上传图片
// 上传图片
const imgArr =['1.jpg','2.jpg','3.jpg']
let promiseArr =[];
imgArr.forEach(item=>{
promiseArr.push(new Promise((resolve,reject)=>{
resolve()
}))
})
Promise.all(promiseArr).then(res=>{
console.log('图片上传成功')
// 插入数据库操作
})
promise.race 应用:图片加载成功或失败
// 图片加载成功,返回promise对象
function getImg(){
return new Promise((resolve,reject)=>{
let img = new Image()
img.onload=function(){
resolve(img)
}
// img.src='http://xxx/xx'
// img.src='https://lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/e08da34488b114bd4c665ba2fa520a31.svg'
})
}
// 图片加载超时,返回promise对象
function timeout(){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
reject('图片请求超时')
},2000)
})
}
// 图片如果能够顺利加载,getImg先完成,图片不存在,超时时间先到达,输出请求超时
// 异步操作和计时器谁先完成
Promise.race([getImg(),timeout()]).then(res=>{
console.log(res)
}).catch(err=>{
console.log(err)
})
49. 什么是asyn await
Es8新增promise语法糖,建立在promise异步编程终极解决方案,使用同步代码实现异步代码。 相对于Promise和回调,可读性和简洁性更好,更好的处理than链。
Async
异步的意思,函数前面加上async,代表这个函数是异步的
该函数返回一个promise,async返回的promise成功还是失败,看函数的return
async function main(){
// 1. 如果返回的是非 Promise 对象,所有数据类型
// 返回成功的promise
return 'iloveyou';
//2. 如果返回的是 Promise 对象
// 看返回的promise是成功还是失败,成功则成功,失败则失败。
return new Promise((resolve ,reject) => {
resolve('123');
reject('失败');
});
//3. 函数抛出异常 promise对象也是失败的
throw '有点问题';
}
// let result = main();
// console.log(result);
main().then(value => {}, reason=>{
console.error(reason);
});
await
await 异步等待 等待一个异步方法执行完成,后面常放返回promise对象表达式,必须放在async中。
await相当于promise的then,try 放到成功的操作 catch 放失败的操作
// await必须写在async函数中,但async函数中可以没有await
async function main() {
// 1、看返回的promise是成功还是失败,看promise的返回结果
let result = await Promise.resolve('OK');
console.log(result);
// 2、返回其他值,返回变量值
let one = await 1;
console.log(one);//1
}
50. new关键字做了什么
- 创建一个空对象(实例化对象)
this指向新对象- 属性方法赋值
- 将这个新对象返回
51. 谈谈你对原型的理解
为什么要有原型?构造函数中的实例每调用一次方法,就会在内存中开辟一块空间,从而造成内存浪费。
// 创建一个构造函数
function Person(name, age){
this.name = name;
this.age = age;
this.sayHello = function(){
console.log("大家好,我是"+this.name)
}
}
// 实例化对象
var p1 = new Person('张无忌',22);
var p2 = new Person('赵敏',20);
var p3 = new Person('周芷若',21);
// 函数调用
p1.sayHello();
p2.sayHello();
p3.sayHello();
console.log(p1.sayHello === p2.sayHello, p2.sayHello === p3.sayHello);
//false
//flase
在函数对象中,有一个属性prototype,它指向了一个对象,这个对象就是原型对象,这个对象的所有属性和方法,都会被构造函数所拥有。
function Person(name, age){
}
console.log(Person.prototype)
// {constructor: ƒ}
普通函数调用,prototype没有任何作用,构造函数调用,该类所有实例有隐藏一个属性(proto)指向函数的prototype。(实例的隐式原型指向类的显示原型)
//实例的隐式原型指向构造函数的显示原型
console.log(p1.__proto__ === Person.prototype);
//true
原型就相当于一个公共区域,可以被类和该类的所有实例访问到。
所以我们在定义类时,公共属性定义到构造函数里面,公共的方法定义到构造函数外部的原型对象上。
原型优点:资源共享,节省内存;改变原型指向,实现继承。缺点:查找数据的时候有的时候不是在自身对象中查找。
52. 谈谈你对原型链的理解
原型链:实际上是指隐式原型链,从对象的__proto__开始,连接所有的对象,就是对象查找属性或方法的过程。
- 当访问一个对象属性时,先往实例化对象在自身中寻找,找到则是使用。
- 找不到(通过
_proto_属性)去它的原型对象中找,找到则是使用。 - 没有找到再去原型对象的原型(
Object原型对象)中寻找,直到找到Object为止,如果依然没有找到,则返回undefined。
53. 原型如何实现继承
function Person() {
this.head = 1;
this.hand = 2;
}
function Man() {
}
Man.portotype = new Person()
54. 什么是闭包
很多编程语言都支持闭包,闭包不是语言特性,而是一种编程习惯。闭包(Closure)是指具有一个封闭对外不公开的包裹结构,或空间。
在JS中,我们可以理解为闭包是函数在特定情况下执行产生的一种现象。
所谓闭包,是一种引用关系,该引用关系存在内部函数中,内部函数引用外部函数数据,引用的数据可以在函数词法作用域(函数外部)之外使用。
产生闭包必满足三个条件:函数嵌套、内部函数引用外部函数数据、外部函数调用,凡是所有的闭包都满足以上三个条件,否则不构成闭包。
闭包本质:内部函数里的一个对象,这个对象非Js对象(有属性有方法的对象),这个对象是函数在运行时,本该释放的活动对象,这个活动对象里包含着我们引用的变量。
55 闭包的应用有哪些
闭包的应用:模拟私有变量、柯里化、偏函数、防抖、节流、实现缓存。
模拟私有变量:将私有变量放在外在的立即执行函数中,并通过立即执行这个函数,创造一个闭包环境(私有变量:只允许函数内部,或对象方法访问的变量)。
柯里化:把接受n个参数的一个函数转化成只接受一个参数n个函数互相嵌套的函数过程,目标是把函数拆解为精准的n部分,也就是将fn(a,b,c)转化成fn(a)(b)(c)的过程。
偏函数:固定函数中的某一个或几个参数,然后返回一个新的函数。
防抖:只执行最后一次。
节流:隔一段时间执行一次。
闭包与内存泄露:闭包造成内存泄漏是误传,误传由于早期IE垃圾回收机机制是基于基于引用计数法,闭包当中如果包含循环引用,那么IE浏览器无法回收闭包中引用的变量,但这内存泄漏和闭包没有关系,而是IE的bug。
更多详细的内容可看我之前文章:《这次把闭包给你讲的明明白白》和《 闭包典型应用用及性能问题》
56. 常见的内存泄漏有哪些
- “手滑”导致的全局变量
function f1() {
name = '小明'
}
在非严格模式下引用未声明的变量,会在全局对象中创建一个新变量,在浏览器中,全局对象是window,这就意味着name这个变量将泄漏到全局。全局变量是在网页关闭时才会释放,这样的变量一多,内存压力也会随之增高。
- 遗忘清理的计时器
程序中我们经常会用到计时器,也就是setInterval和setTimeout
var timeId = setInterval(function(){
// 函数体
},1000)
- 遗忘清理的dom元素引用
var divObj = document.getElementById('mydiv')
// dom删除myDiv
document.body.removeChild(divObj);
console.log(divObj);
// 能console出整个div 说明没有被回收,引用存在
// 移出引用
divObj = null;
console.log(divObj)
// null
57. 谈谈你对this理解
当一个函数被调用时,会创建一个执行上下文,其中this就是执行上下文的一个属性,this是函数在调用时JS引擎向函数内部传递的一个隐含参数。
this指向完全是由它的调用位置决定,而不是声明位置。除箭头函数外,this指向最后调用它的那个对象。
- 全局作用域中,无论是否严格模式都指向
window; - 普通函数调用,指向
window;严格模式下指向undefined; - 对象方法使用,该方法所属对象;
- 构造函数调用,指向实例化对象;
- 匿名函数中,指向
window; - 计时器中,指向
window; - 事件绑定方法,指向事件源;
- 箭头函数指向其上下文中
this;
58.谈谈你对call、apply、bind理解
call、apply和bind,都是用来改变this指向的,三者是属于大写 Function原型上的方法,只要是函数都可以使用。
call和apply的区别,体现在对入参的要求不同,call的实参是一个一个传递,apply的实参需要封装到一个数组中传递。
call、apply相比bind方法,函数不会执行,所以我们需要定义一个变量去接收执行。
更多详细的内容可看我之前文章:《this指向详解及自定义call、apply、bind》
58. JS 中执行上下文
代码执行前,浏览器的Js引擎先会创建代码执行的环境来处理此Js代码的转换和执行,代码的执行环境称为执行上下文。
执行上下文是一个抽象概念,包含当前正在运行的代码以及帮助其执行的所有内容。
执行上下文主要分为三类:
- 全局执行上下文 —— 全局代码所处的环境,不在函数内部代码都在全局执行。
- 函数执行上下文 —— 在函数调用时创建的上下文。
Eval执行上下文 —— 运行在Eval函数中代码时创建的环境,Eval由于性能问题在我们平时开发中很少用到,所有这里我们不在讨论。
全局执行上下文:
(1)将window作为全局执行上下文对象
(2)创建this,this 指向window
(3)给变量和函数安排内存空间
(4)变量赋值undefined,函数声明放入内存
(5)放入作用域链
全局与函数执行上下文不同:
(1)全局:在文件执行前创建;函数:在函数调用时创建
(2)全局:只创建一次;函数:调用几次创建几次
(3)将window作为全局对象;函数:创建参数对象arguments,this指向调用者
更多详细的内容可看我之前文章:《图解JS中执行上下文与执行栈》
59. 请描述一下Js的事件轮询机制
JS是单线程运行,同一时间只能干一件事情,异步要基于回调实现。事件轮询就是异步回调实现的原理。
首先来说JS从前到后一行一行执行,当遇到代码报错,后面的代码将不再执行,先把同步代码执行完,在执行异步。我们先来看一个简单的打印。
console.log('hello');
setTimeout(function cb1(){
console.log('cb1') // cb即callback
},5000)
console.log('bye')
(1)执行第一行代码,把代码推入调用栈(call stack),执行完打印hello,清空调用栈。
(2)执行第二行代码,将代码放web APIs,setTimeout是浏览器定义的,5秒钟之后触发,将cb1放到计时器里,5秒钟之后把cb1放到callback Queue里。
(3)执行第三行代码,将代码放入调用栈中,执行完打印bye,执行完,清空调用栈。
(4)这时候计时器还在web APIs中,一旦同步代码执行完,浏览器内核会启动event loop,event loop这个机制会一遍一遍循环(5s后)在callback Queque还有没有函数,有的话就会拿到调用栈执行,执行完,清空调用栈。
总结event loop过程:
(1)同步代码,一行一行放入调用栈中执行。
(2)遇到异步,先记录下来,等待时机。(比如遇到计时器会将计时器放入到web APIs里)。
(3)时机到了就会移动到callback Queque中。
(4)同步代码执行完(call stack为空),event loop开始工作。
(5)event loop 轮训查找callback queque,如果有则移动到call stack执行。如果没有,继续轮训查找。(像永动机一样)。
60. 什么是宏任务,什么是微任务
- 宏任务:ajax请求、计时器、DOM事件
- 微任务:promise/aysn await
- 微任务执行时机比宏任务早
我们通过下面一段简单代码可以验证:
Console.log(100);
// 宏任务
setTimeout(()=>{
Console.log(200)
})
// 微任务
Promise.resolve().than(()=>{
Consoel,log(300)
})
Console.log(400)
// 打印结果
100 400 300 400
事件轮询和DOM渲染问题
JS是单线程的,而且和DOM渲染公用一个线程,JS执行的时候,得留一些时机供DOM渲染。
- 每次调用栈清空,同步任务执行完
- 都是DOM重新渲染的机会,DOM结构如有改变则重新渲染
- 然后去触发下一次
event loop
为什么微任务比宏任务执行更早
- 微任务:dom渲染前触发
- 宏任务:dom渲染后触发
61. 微任务和宏任务的根本区别
如果遇到promise,等待时机放到micro task queue中
为什么? 宏任务是es6语法规定 宏任务是浏览器规定的
62. 简单介绍一下JS的垃圾回收机制
每隔一段时间,JS的垃圾收集器就会对变量做“巡检”。当它判断一个变量不再被需要之后,它就会把这个变量所占的内存空间给释放掉,这个过程叫做垃圾回收。 常用的垃圾回收算法有两种——引用计数法和标记清除法。
- 引用计数法
这是最初级的垃圾回收算法,在现代浏览器里几乎被淘汰的干干净净。
当我们创建一个变量,对应的也就创建了一个针对这个值的引用。
const students = ['小红','小明']
在引用这块计数法的机制下,内存中每一个值都会对应一个引用计数。当垃圾收集器感知到某个值的引用计数为0时,就判断它“没用”了,随即这块内存就会被释放。
比如我们此时如果把student指向一个null:
students = null
那么这个数组所应用的引用计数就会变成0(如下图),它就变成一块没用的内存,即将面临着作为垃圾,被回收的命运。
引用计数法弊端
大家现在来看这样一个例子:
function badCycle() {
var cycleObj1 = {}
var cycleObj2 = {}
cycleObj1.target = cycleObj2
cycleObj2.target = cycleObj1
}
badCycle()
当执行了badCycle这个函数,作用域内的变量也会全部被视为“垃圾”进而移除。
但如果咱们用了引用计数法,那么即使 badCycle 执行完毕,cycleObj1 和 cycleObj2 还是会活得好好的 —— 因为 cycleObj2 的引用计数为 1(cycleObj1.target),而 cycleObj1 的引用计数也为 1 (cycleObj2.target)(如下图)。
这就是引用计数法的弊端,无法甄别循环引用场景下的“垃圾”。
- 标记清除法
引用计数法无法甄别“循环引用”场景下的“垃圾”,自 2012年起,所有浏览器都使用了标记清除算法。可以说,标记清除法是现代浏览器的标准垃圾回收算法。
在标记清除算法中,一个变量是否被需要的判断标准,是它是否可抵达 。
这个算法有两个阶段,分别是标记阶段和清除阶段:
- 标记阶段:垃圾收集器会先找到根对象,在浏览器里,根对象是 Window;在 Node 里,根对象是 Global。从根对象出发,垃圾收集器会扫描所有可以通过根对象触及的变量,这些对象会被标记为“可抵达 ”。
- 清除阶段: 没有被标记为“可抵达” 的变量,就会被认为是不需要的变量,这波变量会被清除
现在大家按照标记清除法的思路,再来看这段代码:
function badCycle() {
var cycleObj1 = {}
var cycleObj2 = {}
cycleObj1.target = cycleObj2
cycleObj2.target = cycleObj1
}
badCycle()
badCycle 执行完毕后,从根对象 Window 出发,cycleObj1 和 cycleObj2 都会被识别为不可达的对象,它们会按照预期被清除掉。这样一来,循环引用的问题,就被标记清除干脆地解决掉了。
63. JS的深浅拷贝
JS基本数据类型不存在深浅拷贝问题,深拷贝和浅拷贝主要针对引用数据类型(数组、函数、对象)
浅拷贝: 拷贝对象的时候,如果属性是基本数据类型,拷贝就是基本数据类型的值,如果属性是引用数据类型,拷贝的就是内存地址,因此修改新拷贝对象属性会影响原对象。
深拷贝:将一个对象从内存中完整的拷贝出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不影响原对象。
区别:深拷贝修改拷贝对象影响原对象,浅拷贝不影响。
浅拷贝数组
(1)concat方法
var arr =[1,2,3,{name:'小明',age:20}];
var newArr=arr.concat();
arr[3].name="小红";
console.log(arr);
//{name:"小红",age:20}
console.log(newArr);
//{name:"小红",age:20}
(2)slice方法
var arr =[1,2,3,{name:'小明',age:20}];
var newArr = arr.slice(0);
newArr[3].name = '小红';
console.log(arr);
console.log(newArr);
(3)延展运算符
var arr =[1,2,3,{name:'小明',age:20}];
var newArr = [...arr];
arr[3].name = '小红';
console.log(arr);
console.log(newArr);
浅拷贝对象
(1)直接拷贝
var obj1={
name:'小明',
cars:[
'奔驰',
‘宝马’,
]
}
var obj2=obj1
(2)assign
对象的合并,将源对象的所有可枚举属性,复制到目标对象
var obj1={
name:'小明',
cars:[
'奔驰',
‘宝马’,
]
}
// 目标对象 源对象
var obj2=object.assign({},obj1)
深拷贝
(1)JSON
先使用JSON.stringify将JS对象转化成JSON串,再使用JSON.parse将JSON字符串转化为对象。
不足:忽略对象中的函数、undefiend、RegExp、Date。
对象中的函数、undefined属性会直接忽略,对象中的RegExp,拷贝后会为空,对象中的Date会转化为字符串。
const school={
name :'慕课网',
type:['前端','java','go'],
fn(){
console.log("我爱学习");
}
}
//将对象转化为字符串
let str =JSON.stringify(school);
//{"name":"慕课网","type":["前端","java","go"]}
//将字符串转化为JS对象
let newSchool=JSON.parse(str);
//修改新对象属性
newSchool.type[0]="c++";
console.log(school);
// ["前端", "java", "go"]
console.log(newSchool);
// ["c++", "java", "go"]
- 手写深拷贝
// 递归实现深拷贝
let school = {
name: '慕课网',
type: ['前端', '后端', '大数据'],
subtype: {
name: '前端',
type: 'vue'
},
fn() {
console.log('我爱学习')
}
}
// 获取数据类型
function getType(data) {
return Object.prototype.toString.call(data).slice(8, -1)
}
console.log(getType(school))
// 递归实现深拷贝
function deepClone(data) {
// 1、判断数据类型
let type = getType(data);
let container;
if (type === 'Array') {
container = [];
}
if (type === 'Object') {
container = {}
}
// 2、遍历
for (let i in data) {
let t = getType(data[i])
if (t === 'Array' || t === 'Objcet') {
container[i] = deepClone(data[i])
} else {
container[i] = data[i]
}
}
return container;
}
const newSchool = deepClone(school);
newSchool.type[0] = '前端端'
console.log(school)
console.log(newSchool)
64. 手写防抖节流
防抖
// fn是我们需要包装的事件回调, delay是每次推迟执行的等待时间
function debounce(fn, delay) {
// 定时器
let timer = null
// 将debounce处理结果当作函数返回
return function () {
// 保留调用时的this上下文
let context = this
// 保留调用时传入的参数
let args = arguments
// 每次事件被触发时,都去清除之前的旧定时器
if(timer) {
clearTimeout(timer)
}
// 设立新定时器
timer = setTimeout(function () {
fn.apply(context, args)
}, delay)
}
}
// 用debounce来包装scroll的回调
const better_scroll = debounce(() => console.log('触发了滚动事件'), 1000)
document.addEventListener('scroll', better_scroll)
节流
// fn是我们需要包装的事件回调, delay是每次推迟执行的等待时间
function debounce(fn, delay) {
// 定时器
let timer = null
// 将debounce处理结果当作函数返回
return function () {
// 保留调用时的this上下文
let context = this
// 保留调用时传入的参数
let args = arguments
// 每次事件被触发时,都去清除之前的旧定时器
if(timer) {
clearTimeout(timer)
}
// 设立新定时器
timer = setTimeout(function () {
fn.apply(context, args)
}, delay)
}
}
// 用debounce来包装scroll的回调
const better_scroll = debounce(() => console.log('触发了滚动事件'), 1000)
document.addEventListener('scroll', better_scroll)
66. 手写call、apply、bind
- 自定义call
在实现 call 方法之前,我们先来看一个 call 的调用示范:
var me = {
name: '张三'
}
function showName() {
console.log(this.name)
}
showName.call(me) // 张三
前面我们说过call方法是大写Function中方法,所有的函数都可以继承使用,所以我们自定义call 方法应该定义在 Function.prototype上,这里我们定义一个myCall。
Function.prototype.myCall=function(){
}
我们想,如果用myCall方法进行绑定,就相当于在传入的对象(这里是me)里面添加了一个原本的函数,然后在使用对象.函数调用,也就是:
var me ={
name :'张三',
person:function(){
console.log(this.name)
}
}
me.person()
根据这个思路,我们往原型对象中添加内容:
// context:我们传入的对象
Function.prototype.newCall = function(context){
// person.newcall调用,也就是函数.方法调用,JS中函数也是对象,所以对象方法调用,指向该方法所属对象,也就是person。
// 注意!这里的this是person,我们还没开始绑定呢
console.log(this)
// 1、我们为传入的对象添加属性
context.fnkey = this;
// 2、调用函数
context.fnkey();
// 3、执行完,方法删除,我们不能改写对象
delete context.fnkey
}
person.newCall(me)
当我们为形参变量添加属性时,此时的代码就如下,然后在调用这个函数,因为是对象方法调用所以this指向了me,也就是obj。
function person(){
console.log(this.name);
}
var me = {
name:'张三',
fnkey:function(){
console.log(this.name);
}
}
现在我们的mycall就实现了call的基本能力——改变this指向,第二步让我们的mycall具备读取函数入参能力,也就是读取call方法第二个到最后一个入参,这里我们用到ES6中的剩余参数...args。
剩余参数可以帮助我们将不定数量的入参变成数组,具体用法如下:
function readArr(...args) {
console.log(args)
}
readArr(1,2,3) // [1,2,3]
我们通过args这个数组拿到我们想要的入参,再把 args数组代表目标入参展开,传入目标方法,一个call方法就实现了。
Function.prototype.myCall = function(context, ...args) {
context.fnkey = this;
context.fnkey(...args);
delete context.fnkey;
}
以上,就实现了mycall的基本框架~~
但是上面的mycall还并不完善,比如说第一个参数传了null怎么办?是不是默认给他指到window或global上去;第一个参数不是对象怎么办?我们改如何保证为对象?如果context里面有这个属性怎么办?我们怎样保证属性的唯一性?
我们进行以下补充优化:
Function.prototype.myCall = function (context, ...args) {
// 补充1 如果第一个参数没传,默认指向window / Global
// globalThis浏览器环境中指window,node.js环境中指向global
if (context == null) context = globalThis
// 补充2:如果第一个参数传的值类型,数字类型,或者布尔类型
// 我们通过new Object 生成一个值类型对象,数字类型对象,布尔类型对象
if (typeof context !== 'objext') context = new Object(context)
// 补充3:防止传入对象作为属性,与context重名属性覆盖
// symbol类型不会出现属性名称覆盖
const fnkey = Symbol();
context[fnkey] = this
globalThis // window/global
console.log(new Object('哈哈'));// String {"哈哈"}
console.log(new Object(1)); // Number { 1 }
console.log(new Object(true)); //Boolean { true }
console.log(new Object(undefined));// {}
let symbol1 = Symbol(); //Symbol()
let symbol2 = Symbol(); //Symbol()
consoele.log(symbol1 === symbol2);//false
这样,我们就实现了完整mycall方法,使用mycall调用时,就相当于在传入的对象里面添加了一个原本的函数,这是实现mycall的核心,一定要理解。完整版mycall方法如下:
Function.prototype.myCall = function (context, ...args) {
// 补充1 如果第一个参数没传,默认指向window / Global
// globalThis浏览器环境中指window,node.js环境中指向global
if (context == null) context = globalThis
// 补充2:如果第一个参数传的值类型,数字类型,或者布尔类型
// 我们通过new Object 生成一个值类型对象,数字类型对象,布尔类型对象
if (typeof context !== 'objext') context = new Object(context)
// 补充3:防止传入对象作为属性,与context重名属性覆盖
// symbol类型不会出现属性名称覆盖
const fnkey = Symbol();
// step1: 给传入对象添加原函数(this就是我们要改造的原函数)
context[fnkey] = this
// step2: 执行函数,并传递参数
context[fnkey](...args)
// step3: 删除 step1 中挂到目标对象上的函数
delete context[fnkey].
}
// 测试如下:
function showFullName(secondName) {
console.log(`${this.name} ${secondName}`)
}
var me = {
name: '张三'
}
showFullName.myCall(me, '李四') // 张三 李四
showFullName.myCall(null, '李四') // 李四
showFullName.myCall(1, '李四') // undefined 李四
复制代码
理解了call,那么实现apply和bind方法就小菜一碟了,apply方法关键在于更改参数的读取方式,bind方法关键在于延迟目标函数的执行时机。
- 自定义apply
Function.prototype.myCall = function (context, ...args) {
if (context == null) context = globalThis
if (typeof context !== 'objext') context = new Object(context)
const fnkey = Symbol();
context[fnkey] = this;
// 此时,传入的数组,不需要对数组进行拆包
context.fnkey(args);
delete context[fnkey];
}
// 测试如下:
function showFullName(secondName) {
console.log(`${this.name} ${secondName}`)
}
var me = {
name: '张三'
}
showFullName.myCall(me, ['李四','王五']) // 张三 李四 王五
- 自定义bind
前面我们说过,bind方法不会立即执行函数,实际上bind方法是返回了一个原函数的拷贝,函数体内的参数会和bind方法第一个以外的其他参数合并。
在实现 bind 方法之前,我们先来看一个 bind 的调用示范:
var me = {
value: 1
}
function person(name, age) {
return {
value: this.value,
name: name,
age: age
}
}
var bar = person.bind(me, '张三', 18);
console.log(bar);
// 这里将会输出person函数
console.log(bar());
// {value: 1, name: "张三", age: 18}
var bar2 = person.bind(me, '张三');
console.log(bar2(18));
// {value: 1, name: "张三", age: 18}
完整版myBind如下:
Function.prototype.myBind = function (context, ...args) {
// step1: 保存下当前 this(这里的 this 就是我们要改造的的那个函数)
const self = this;
// step2: 返回一个函数
// bind整体上会return一个函数,并还可以接受参数
return function (...argus) {
// step3: 拼接完整参数,将bind执行参数和函数调用时传入参数拼接
const fullArgs = args.concat(argus)
// step4: 调用函数
return self.apply(context,fullArgs)
}
}
// 测试如下:
function showFullName(secondName) {
console.log(`${this.name} ${secondName}`)
}
var me = {
name: '张三'
}
var result = showFullName.myBind(me, '李四')
result() // 张三 李四