前言
近期整理了JavaScript知识体系,50个知识点由浅入深掌握Js建议收藏,如有问题,欢迎指正。
1. 说说你对JS的理解
1995年,布莱登·艾奇(美国人)在网景公司,用10天写的一门语言。
Js是一门:动态的,弱类型的,解释型的,基于对象的脚本语言,同时Js又是单线程的。
- 动态类型语言:
代码在执行过程中,才知道这个变量属于的类型。 - 弱类型:声明变量一般用var,数据类型不固定,可以随时改变。可以将字符串’12’和整数3进行连接得到字符串’123’,在相加的时候会进行强制类型转换。
- 解释型:一边执行,一边编译,不需要程序在运行之前需要整体先编译。
- 基于对象:最终所有对象都指向
Object
。 - 脚本语言 :一般都是可以嵌在其它编程语言当中执行。
- 单线程:依次执行,前面代码执行完后面才执行。
组成部分:
ECMAscript | DOM | BOM |
---|---|---|
JavaScript的语法部分 | 文档对象模型 | 浏览器对象模型 |
主要包含JavaScript语言语法 | 主要用来操作页面元素和样式 | 主要用来操作浏览器相关功能 |
2. 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
引用数据类型存储在堆中。引用数据类型占据空间大,如果存储在栈中,将会影响程序运行的性能。
引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。
3. Undefined 与 undeclared 的区别?
变量声明未赋值,是 undefined
。
未声明的变量,是 undeclared
。浏览器会报错a is not defined
,ReferenceError。
4. Null和undefined的区别
null
和 undefined
都是基本数据类型,这两个数据类型只有一个值,null
和 undefined
。
null
表示空的,什么都没有,不存在的对象,他的数据类型是object
。
初始值赋值为null
,表示将要赋值为对象,
不再使用的值设为null
,浏览器会自动回收。
undefined
表示未定义,常见的为undefined
情况:
一是变量声明未赋值,
二是数组声明未赋值;
三是函数执行但没有明确的返回值;
四是获取一个对象上不存在的属性或方法。
5. 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解析器会将值转化为数值再运算
// 总结:字符相连,数值相加(- * /)
6. 数据类型的判断
(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]
7. a++ 和 ++a 区别
无论是前++还是后++,都会使原来变量立刻自增1。
不同在于a++是原值,++a是新值。
var a = 4;
console.log(a++); //4 原值
var b = 4;
console.log(++b) //5 新值
8. 0.1+0.2 === 0.3吗
在开发过程中遇到类似这样的问题:
let n1 = 0.1, n2 = 0.2
console.log(n1 + n2) // 0.30000000000000004
这里得到的不是想要的结果,要想等于0.3,就要把它进行转化:
(n1 + n2).toFixed(2) // 注意,toFixed为四舍五入
toFixed(num)
方法可把 Number 四舍五入为指定小数位数的数字。那为什么会出现这样的结果呢?
toFixed(num)
方法可把 Number 四舍五入为指定小数位数的数字。那为什么会出现这样的结果呢?
计算机是通过二进制的方式存储数据的,所以计算机计算0.1+0.2的时候,实际上是计算的两个数的二进制的和。0.1的二进制是0.0001100110011001100...
(1100循环),0.2的二进制是:0.00110011001100...
(1100循环),这两个数的二进制都是无限循环的数。那JavaScript是如何处理无限循环的二进制小数呢?
一般我们认为数字包括整数和小数,但是在 JavaScript 中只有一种数字类型:Number,它的实现遵循IEEE 754标准,使用64位固定长度来表示,也就是标准的double双精度浮点数。在二进制科学表示法中,双精度浮点数的小数部分最多只能保留52位,再加上前面的1,其实就是保留53位有效数字,剩余的需要舍去,遵从“0舍1入”的原则。
根据这个原则,0.1和0.2的二进制数相加,再转化为十进制数就是:0.30000000000000004
。
9. JS的作用域和作用域链
作用域就变量起作用的范围和区域。 作用域的目的是隔离变量,保证不同作用域下同名变量不会冲突。
JS中,作用域分为三种,全局作用域、函数作用域和块级作用域。 全局作用域在script
标签对中,无论在哪都能访问到。在函数内部定义的变量,拥有函数作用域。块级作用域则是使用let
和const
声明的变量,如果被一个大括号括住,那么这个大括号括住的变量区域就形成了一个块级作用域。
作用域层层嵌套,形成的关系叫做作用域链,作用域链也就是查找变量的过程。 查找变量的过程:当前作用域 --》上一级作用域 --》上一级作用域 … --》直到找到全局作用域 --》还没有,则会报错。
作用域链是用来保证——变量和函数在执行环境中有序访问。
10. LHS和RHS查询
LHS和RHS查询是JS引擎查找变量的两种方式,这里的“Left”和“Right”,是相对于赋值操作来说,当变量出现在赋值操作左侧时,执行LHS操作。
LHS 意味着 变量赋值或写入内存,,他强调是写入这个动作。
var name = '小明'
当变量出现在赋值操作右侧或没有赋值操作时,是RHS。
var Myname = name
console.log(name)
RHS意味着 变量查找或读取内存,它强调的是读这个动作。
11. 词法作用域和动态作用域
Js底层遵循的是词法作用域,从语言的层面来说,作用域模型分两种:
词法作用域:也称静态作用域,是最为普遍的一种作用域模型
动态作用域:相对“冷门”,bash脚本、Perl等语言采纳的是动态作用域
词法作用域:在代码书写时完成划分,作用域沿着它定义的位置往外延伸。
动态作用域:在代码运行时完成划分,作用域链沿着他的调用栈往外延伸。
12. 什么是匿名函数,有什么作用
匿名函数也叫一次性函数,没用名字,且在定义时执行,且执行一次,不存在预解析(函数内部执行的时候会发生)。
匿名函数的基本形式为:
(function(){
...
}());
除此之外,还有常见的以下写法
(function(){
console.log('我是一个匿名函数')
})();
var a = function(){
console.log('我是一个匿名函数')
}()
// 使用多种运算符开头,一般是用!
!function(){
console.log('我是一个匿名函数')
}();
匿名函数的作用有:
(1)对项目的初始化,页面加载时调用,保证页面有效的写入Js,不会造成全局变量污染
(2)防止外部命名空间污染
(3)隐藏内部代码暴露接口
13. 什么是回调函数,常见的回调函数有哪些
回调函数是一段可执行的代码段,它作为一个参数传递给其他的代码,其作用是在需要的时候方便调用这段(回调函数)。
在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)
14. 什么是构造函数,与普通函数的区别
在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);
15. 函数中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));//传递的是三个实参就做加法
16. 列举常用字符串方法
方法名 | 功能 | 原字符串是否改变 |
---|---|---|
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 |
17. 列举常用数组方法
方法名 | 功能 | 原数组是否改变 |
---|---|---|
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 |
注意:重点关注方法的:功能、参数、返回值
18. 什么是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);//屏幕的高
19. DOM树简单描述一下
以Html为根节点,形成的一棵倒立的树状结构,我们称作DOM树。这个树上所有的东西都叫节点,节点有很多类(元素、属性、文本),通过DOM方法去获取或者去操作节点,就叫DOM对象。
Document对象
指这份文件,也就是这份 HTML 档的开端。当浏览器载入 HTML 文档, 它就会成为 Document 对象。
重绘:DOM元素的样式发生改变,浏览器会重新渲染这个元素。
回流:DOM元素结构或者位置发生改变(删除、增加、改变位置大小),浏览器重新计算渲染整个DOM树。
20. 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
21. 什么是事件传播
当事件发生在DOM
对象上,该事件并不完全发生在这个元素上。
关于事件传播,IE和网景公司有不同的理解,IE认为,事件应该是冒泡阶段,网景公司认为事件应该是捕获阶段,随后W3C综合了两个公司的方案,JS同时支持冒泡和捕获流,并以此确定事件流标准。这个标准也叫DOM2事件流。
事件传播的三个阶段:
(1)事件捕获
事件从window
开始,从外向内,直到到达目标事件或event.target
(1)目标阶段
事件到达目标元素,触发监听事件
(2)事件冒泡
事件从目标元素开始冒泡,从内向外,直到到达window
。
当事件被触发时,首先经历的是一个捕获过程,事件会从最外层元素开始“穿梭”,逐层“穿梭”到最内层元素。这个穿梭过程会持续到事件抵达他目标的元素(也就是真正触发这个事件的元素)为止。此时事件流接切换到了“目标阶段”——事件被目标元素所接收然后事件会会弹,进入到冒泡阶段——他会沿着来时的路“逆流而上”,一层一层再走回去。
也就是说当事件在层层DOM
元素穿梭时,所到之处都会触发事件处理函数。
23. 三种事件模型是什么
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)
}
24. 事件对象有哪些常用属性
当DOM
接受了一个事件,对应的事件处理函数被触发时,就会产生一个事件对象event
作为事件处理函数的入参。这个对象中包含着与事件有关的信息,比如事件是由哪个元素触发的,事件类型等。常用属性有:
- target
事件绑定的元素
- currentTarget
触发事件的元素,两者没有冒泡的情况下,是一样的值,但在使用了事件委托的情况下,就不一样了。
preventDafault
阻止默认行为,比如阻止超链接跳转、在form中按回车会自动提交表单。
e.preventDefault();
stopPropagation
阻止事件冒泡,将事件处理函数的影响控制在目标元素范围内
e.stopPropagation();
阻止事件冒泡,需要注意一点的是:谷歌火狐的组织行为是:event.stopPropagation()
,而IE:event.cancelBubble=true
25. 什么是事件委托
原理:
如果子元素有很多,且子元素的事件监听逻辑都相同,将事件监听绑定到父元素身上或者共有的祖先元素上 。事件委托原理是利用事件冒泡,子元素触发,父元素执行回调函数。
好处:
(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操作,减少了内存开销,大大提升了开发效率。
26. ECMAScript 是什么
ES是JS的标准,约束条件。广义的JS=ES+DOM+BOM,狭义的JS就是ES。EC是为了保证JS在浏览器运行结果一致。
是由欧洲计算机协会(ECMA)这个组织制定的。这个组织的目标是制定和发布脚本语言规范。组织会定期定期召开会议,会议由一些公司的代表与特邀专家出席。
27. ECMAScript 2015(ES6)有哪些新特性?
在2011年ECMA组织就开始着手制作第6个版本规范,由于这个版本引入的功能语法太多,最终标准的制作者决定在每年6月份发布一次,版本号为年号代替,ES6正式发布于2015 年 6 月。我们现在所说的ES6是一个泛指,泛指ES2015之后的版本。
- 块级作用域
- 对象数组解构赋值
- 模板字符串
- 箭头函数
- 延展运算符
- 剩余参数
- 声明类
- set、map集合
- Promise
28. 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]
29. const对象的属性可以修改吗
const
保证的并不是变量的值不能改动,而是指向那个内存地址不能改动,对于基本数据类型(数值、字符串、布尔值),其值就保存在变量指向的那个内存地址,因此等同于常量。
但对于引用数据类型(主要是对象和数组),变量指向数据的内存地址,保存的只是一个指针,const
只能保证这个指针是固定不变的,至于他指向的数据结构是不是可变的,就不完全能控制了。
30. 什么是解构赋值
在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
31. 什么是模板字符串
模板字符串是增强版的字符串,用反引号
表示,模板字符串可以当普通字符串使用,也可以用来定义多行字符串,作用是简化字符串的拼接。特点如下:
可以出现换行符
// 可以出现换行符
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)
32. 箭头函数和普通函数区别
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();
}
};
33. 什么是剩余运算符
剩余运算符中最重要的特点就是代替以前的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)
34. 延展运算符使用过吗
拆包和打包数组、对象
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);
35. 什么是类
类(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();
36. 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()
37. 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
可以完美监听到任何方式的数据改变,唯一缺陷就是浏览器的兼容性不好。
38. promise
- 产生
ES6中新技术,解决异步回调地域问题。
回调地狱: 回调嵌套或者函数很乱的调用,简单来说,就是:发四个请求,第四个依赖第三个结果,第三个依赖第二个的结果,第二个依赖第一个的结果。 回调函数弊端: 不利于阅读,不利于捕获异常,不能直接return
。
setTimeout(() => {
console.log(1)
setTimeout(() => {
console.log(2)
setTimeout(() => {
console.log(3)
},3000)
},2000)
},1000)
常见的回调函数: 计时器、AJAX、数据库操作、fs,其中,经常使用的场景是 ,AJAX请求以及各种数据库操作会产生回调地狱。 promise解决异步避免回调地狱
function f1() {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(111), 1000);
}).then(data => console.log(data));
}
function f2() {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(222), 2000);
}).then(data => console.log(data));;
}
function f3() {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(333), 3000);
}).then(data => console.log(data));;
}
f1().then(f2).then(f3)
- 基础
- promise对象表示一个异步操作的最终完成或失败,及其结果值 , 是一个代理值。
- 语法上:Promise是一个构造函数,用来生成Promise的实例对象。
- 功能上:Promise对象用来包裹一个异步操作,并获取成功、失败结果值。
- 三种状态
- pending:初始状态,既不成功,也不失败。
- fulfilled:操作成功完成
- rejected:操作失败 状态要不成功要不失败,此过程不可逆,且只能修改一次。
- 基本使用
promise构造函数有两个参数(resolve,reject), 操作成功,调用resolve函数,将promise对象的状态改为fulfilled。 操作成功,调用rejected函数,将promise对象的状态改为rejected。
- resolve函数
let obj = new Promise((resolve, reject) => {
resolve('ok') ;
});
//1. 如果传入的是非Promise类型的数据,则返回成功的promise
let p = Promise.resolve('abc');
//2. 如果是 Promise ,那么该对象的结果就决定了 resolve 的返回结果
let p2 = Promise.resolve(obj);
//3. 嵌套使用
let p3 = Promise.resolve(Promise.resolve(Promise.resolve('ABC')));
console.log(p3);
- reject函数
//Promise.prototype.reject 返回的始终是失败的 Promise
let p = Promise.reject(1231231);
let p2 = Promise.reject('abc');
let p3 = Promise.reject(Promise.resolve('OK'));
console.log(p3);
- API
- than
为当前promise指定成功或失败的回调,返回一个新的promise供我们调用。成功的参数一般是value,失败的参数reason。 than里的数据—>resolve里的数据 than返回结果—>than里的回调函数决定
let p=new Promise((resolve,reject)=>{
resolve('ok')
})
// value是resolve的参数,第一个箭头函数叫resolve
p.then(value=>{
console.log(value)//ok
},reason=>{
console.log('onRejected1', reason)
// (1)如果返回非Promise类型的数据,则返回成功的promise
return 1000
// (2)返回Promise ,那么该对象的结果就决定了函数的返回结果
return Promise.resolve(300)
// (3)抛出错误,失败的promise
throw 100
// (4)无返回值,返回undefined,也是成功的promise
})
- catch
指定失败的回调
let p =new Promise((resolve,reject)=>{
reject('失败了');
})
p.then(value=>{},reason=>{
console.error(reason);
})
p.catch(reason=>{
console.error(reason)
})
3 .all
Promise.all([promise1,promise2,promise3]) 批量一次性发送多个异步请求 只有当都成功是返回的promise才会成功 返回一个新的promise 问题: 发3请求成功后再4个请求
function ajax(url) {
return axios.get(url)
}
const p1 = ajax(url1)
const p2 = ajax(url2)
const p3 = ajax(url3)
Promise.all([p1, p2, p3])
// values和数组中数据的顺序有关系
.then(values => {
return ajax(url4)
})
.then(value => {
console.log(value) // 就是第4个请求成功的value
})
.catch(error => {
})
4 .race
多个promise任务同步执行,返回最先结束的promise任务结束,不论是成功还是失败,简单来说就先到先得。
// race 赛跑
let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('OK');
}, 1000);
});
let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Yes');
}, 500);
});
let result = Promise.race([p1, p2]);
console.log(result);
39. 什么是asyn await
new Promise(resolve,reject)=>{
setTimeout(()=>resolve(111),1000)
}).then(data=>{
console.log(data)
})
es8新增promise语法糖 建立在Promise之上异步编程终极解决方案,使用同步代码实现异步代码。 相对于 Promise 和回调,它的可读性和简洁度都更高,更好地处理 then 链。
- 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
}
40. new关键字做了什么
- 创建一个空对象(实例化对象)
this
指向新对象- 属性方法赋值
- 将这个新对象返回
41. 谈谈你对原型的理解
为什么要有原型?构造函数中的实例每调用一次方法,就会在内存中开辟一块空间,从而造成内存浪费。
在函数对象中,有一个属性prototype
,它指向了一个对象,这个对象就是原型对象,这个对象的所有属性和方法,都会被构造函数所拥有。
function Person(name, age){
}
console.log(Person.prototype)
// {constructor: ƒ}
普通函数调用,prototype
没有任何作用,构造函数调用,该类所有实例有隐藏一个属性(proto)指向函数的prototype。(实例的隐式原型指向类的显示原型)
//实例的隐式原型指向构造函数的显示原型
console.log(p1.__proto__ === Person.prototype);
//true
原型就相当于一个公共区域,可以被类和该类的所有实例访问到。
所以我们在定义类时,公共属性定义到构造函数里面,公共的方法定义到构造函数外部的原型对象上。
原型优点:资源共享,节省内存;改变原型指向,实现继承。缺点:查找数据的时候有的时候不是在自身对象中查找。
42. 谈谈你对原型链的理解
原型链:实际上是指隐式原型链,从对象的__proto__
开始,连接所有的对象,就是对象查找属性或方法的过程。
- 当访问一个对象属性时,先往实例化对象在自身中寻找,找到则是使用。
- 找不到(通过
_proto_
属性)去它的原型对象中找,找到则是使用。 - 没有找到再去原型对象的原型(
Object
原型对象)中寻找,直到找到Object
为止,如果依然没有找到,则返回undefined
。
43. 谈谈你对this,call,apply,bind理解
当一个函数被调用时,会创建一个执行上下文,其中this
就是执行上下文的一个属性,this
是函数在调用时JS引擎向函数内部传递的一个隐含参数。
this
指向完全是由它的调用位置决定,而不是声明位置。除箭头函数外,this指向最后调用它的那个对象。
- 全局作用域中,无论是否严格模式都指向
window
; - 普通函数调用,指向
window
;严格模式下指向undefined
; - 对象方法使用,该方法所属对象;
- 构造函数调用,指向实例化对象;
- 匿名函数中,指向
window
; - 计时器中,指向
window
; - 事件绑定方法,指向事件源;
- 箭头函数指向其上下文中
this
;
call
、apply
和bind
,都是用来改变this
指向的,三者是属于大写 Function
原型上的方法,只要是函数都可以使用。
call
和apply
的区别,体现在对入参的要求不同,call
的实参是一个一个传递,apply
的实参需要封装到一个数组中传递。
call
、apply
相比bind
方法,函数不会执行,所以我们需要定义一个变量去接收执行。
更多详细的内容可看我之前文章:《this指向详解及自定义call、apply、bind》
44. 什么是闭包,哪些地方用到过
很多编程语言都支持闭包,闭包不是语言特性,而是一种编程习惯。闭包(Closure)是指具有一个封闭对外不公开的包裹结构,或空间。
在JS中,我们可以理解为闭包是函数在特定情况下执行产生的一种现象。
所谓闭包,是一种引用关系,该引用关系存在内部函数中,内部函数引用外部函数数据,引用的数据可以在函数词法作用域(函数外部)之外使用。
产生闭包必满足三个条件:函数嵌套、内部函数引用外部函数数据、外部函数调用,凡是所有的闭包都满足以上三个条件,否则不构成闭包。
闭包本质:内部函数里的一个对象,这个对象非Js对象(有属性有方法的对象),这个对象是函数在运行时,本该释放的活动对象,这个活动对象里包含着我们引用的变量。
闭包的作用:模拟私有变量、柯里化、偏函数、防抖、节流、实现缓存。
模拟私有变量:将私有变量放在外在的立即执行函数中,并通过立即执行这个函数,创造一个闭包环境(私有变量:只允许函数内部,或对象方法访问的变量)。
柯里化:把接受n个参数的一个函数转化成只接受一个参数n个函数互相嵌套的函数过程,目标是把函数拆解为精准的n部分,也就是将fn(a,b,c)转化成fn(a)(b)(c)
的过程。
偏函数:固定函数中的某一个或几个参数,然后返回一个新的函数。
防抖:只执行最后一次。
节流:隔一段时间执行一次。
闭包与内存泄露:闭包造成内存泄漏是误传,误传由于早期IE垃圾回收机机制是基于基于引用计数法,闭包当中如果包含循环引用,那么IE浏览器无法回收闭包中引用的变量,但这内存泄漏和闭包没有关系,而是IE的bug。
更多详细的内容可看我之前文章:《这次把闭包给你讲的明明白白》和《 闭包典型应用用及性能问题》
45. 常见的内存泄漏有哪些
- “手滑”导致的全局变量
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
46. JS微任务红任务执行顺序
(1)JS引擎首先执行所有同步代码
(2)宏队列:保存待执行宏任务
(3)微队列:保存待执行的微任务
47. 简单介绍一下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 都会被识别为不可达的对象,它们会按照预期被清除掉。这样一来,循环引用的问题,就被标记清除干脆地解决掉了。
48. 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)
49. 手写防抖节流
防抖
// 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)
50. 手写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() // 张三 李四