【JavaScript】50个知识点由浅入深掌握Javascript🔥🔥

目录

前言

近期整理了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

引用数据类型存储在堆中。引用数据类型占据空间大,如果存储在栈中,将会影响程序运行的性能。

引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在中的地址,取得地址后从中获得实体。

【JavaScript】50个知识点由浅入深掌握Javascript🔥🔥

3. Undefined 与 undeclared 的区别?

变量声明未赋值,是 undefined

未声明的变量,是 undeclared。浏览器会报错a is not defined ,ReferenceError。

4. Null和undefined的区别

nullundefined  都是基本数据类型,这两个数据类型只有一个值,nullundefined

null表示空的,什么都没有,不存在的对象,他的数据类型是object
初始值赋值为null,表示将要赋值为对象,
不再使用的值设为null,浏览器会自动回收。

undefined表示未定义,常见的为undefined情况:
一是变量声明未赋值,
二是数组声明未赋值;
三是函数执行但没有明确的返回值;
四是获取一个对象上不存在的属性或方法。

5. JS数据类型转换

JS的显式数据类型转换一共有三种:

(1)第一种是:转字符串。有.toString()方法和String()函数,Sting()函数相比于toString()函数适用范围更广,可以将nullundefined转化为字符串,toString()转化会报错。

(2)第二种是:转数值。可以用Number()函数转数值,.parseInt转整数,parseFloat函数转小数。

Number()函数适用于所有类型的转换,比较严格,字符串合法数字则转化成数字,不合法则转化为NAN;空串转化为0,nullundefined转0和NANture转1,false转0。

parseInt()是从左向右获取一个字符串的合法整数位,parseFloat()获取字符串的所有合法小数位。

(3)第三种是:转布尔。像false、0、空串、nullundefinedNaN这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 对象来访问它的构造函数。既可以检测基本类型又可以检测对象,但不能检测nullundefined

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标签对中,无论在哪都能访问到。在函数内部定义的变量,拥有函数作用域。块级作用域则是使用letconst声明的变量,如果被一个大括号括住,那么这个大括号括住的变量区域就形成了一个块级作用域。

作用域层层嵌套,形成的关系叫做作用域链,作用域链也就是查找变量的过程。  查找变量的过程:当前作用域 –》上一级作用域 –》上一级作用域 …. –》直到找到全局作用域 –》还没有,则会报错。

作用域链是用来保证——变量和函数在执行环境中有序访问。

10. LHS和RHS查询

LHS和RHS查询是JS引擎查找变量的两种方式,这里的“Left”和“Right”,是相对于赋值操作来说,当变量出现在赋值操作左侧时,执行LHS操作。

LHS 意味着 变量赋值写入内存,,他强调是写入这个动作。

var name = '小明'

当变量出现在赋值操作右侧或没有赋值操作时,是RHS。

var Myname = name
console.log(name)

RHS意味着 变量查找读取内存,它强调的是这个动作

11. 词法作用域和动态作用域

Js底层遵循的是词法作用域,从语言的层面来说,作用域模型分两种:

词法作用域:也称静态作用域,是最为普遍的一种作用域模型

动态作用域:相对“冷门”,bash脚本、Perl等语言采纳的是动态作用域

词法作用域:在代码书写时完成划分,作用域沿着它定义的位置往外延伸。
动态作用域:在代码运行时完成划分,作用域链沿着他的调用栈往外延伸。

更多详细的内容可看我之前文章:《深入理解JS中的词法作用域与作用域链》

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(后面我们会说到),另一个就是argumentsarguments是一个伪数组,主要作用是:获取函数中在调用时传入的实参

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 对象的子对象。

【JavaScript】50个知识点由浅入深掌握Javascript🔥🔥

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对象。

【JavaScript】50个知识点由浅入深掌握Javascript🔥🔥

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

【JavaScript】50个知识点由浅入深掌握Javascript🔥🔥

当事件被触发时,首先经历的是一个捕获过程,事件会从最外层元素开始“穿梭”,逐层“穿梭”到最内层元素。这个穿梭过程会持续到事件抵达他目标的元素(也就是真正触发这个事件的元素)为止。此时事件流接切换到了“目标阶段”——事件被目标元素所接收然后事件会会弹,进入到冒泡阶段——他会沿着来时的路“逆流而上”,一层一层再走回去。

也就是说当事件在层层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)
  • 基础
  1. promise对象表示一个异步操作的最终完成或失败,及其结果值 是一个代理值
  2. 语法上:Promise是一个构造函数,用来生成Promise的实例对象。
  3. 功能上:Promise对象用来包裹一个异步操作,并获取成功、失败结果值。
  • 三种状态
  1. pending:初始状态,既不成功,也不失败。
  2. fulfilled:操作成功完成
  3. 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
  1. 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
})
  1. 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关键字做了什么

  1. 创建一个空对象(实例化对象)
  2. this指向新对象
  3. 属性方法赋值
  4. 将这个新对象返回

41. 谈谈你对原型的理解

为什么要有原型?构造函数中的实例每调用一次方法,就会在内存中开辟一块空间,从而造成内存浪费。

在函数对象中,有一个属性prototype,它指向了一个对象,这个对象就是原型对象,这个对象的所有属性和方法,都会被构造函数所拥有。

 function Person(name, age){
 }		
 console.log(Person.prototype)
// {constructor: ƒ}

普通函数调用,prototype没有任何作用,构造函数调用,该类所有实例有隐藏一个属性(proto)指向函数的prototype。(实例的隐式原型指向类的显示原型)

//实例的隐式原型指向构造函数的显示原型
 console.log(p1.__proto__ === Person.prototype);
//true

原型就相当于一个公共区域,可以被类和该类的所有实例访问到。
【JavaScript】50个知识点由浅入深掌握Javascript🔥🔥

所以我们在定义类时,公共属性定义到构造函数里面,公共的方法定义到构造函数外部的原型对象上

原型优点:资源共享,节省内存;改变原型指向,实现继承。缺点:查找数据的时候有的时候不是在自身对象中查找。

42. 谈谈你对原型链的理解

原型链:实际上是指隐式原型链,从对象的__proto__开始,连接所有的对象,就是对象查找属性或方法的过程。
【JavaScript】50个知识点由浅入深掌握Javascript🔥🔥

  1. 当访问一个对象属性时,先往实例化对象在自身中寻找,找到则是使用。
  2. 找不到(通过_proto_属性)去它的原型对象中找,找到则是使用。
  3. 没有找到再去原型对象的原型Object原型对象)中寻找,直到找到Object为止,如果依然没有找到,则返回undefined

43. 谈谈你对this,call,apply,bind理解

当一个函数被调用时,会创建一个执行上下文,其中this就是执行上下文的一个属性,this是函数在调用时JS引擎向函数内部传递的一个隐含参数。

this指向完全是由它的调用位置决定,而不是声明位置。除箭头函数外,this指向最后调用它的那个对象

  1. 全局作用域中,无论是否严格模式都指向window
  2. 普通函数调用,指向window;严格模式下指向undefined
  3. 对象方法使用,该方法所属对象;
  4. 构造函数调用,指向实例化对象;
  5. 匿名函数中,指向window
  6. 计时器中,指向window
  7. 事件绑定方法,指向事件源;
  8. 箭头函数指向其上下文中this

callapplybind,都是用来改变this指向的,三者是属于大写 Function原型上的方法,只要是函数都可以使用。

callapply的区别,体现在对入参的要求不同,call的实参是一个一个传递,apply的实参需要封装到一个数组中传递。

callapply相比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这个变量将泄漏到全局。全局变量是在网页关闭时才会释放,这样的变量一多,内存压力也会随之增高。

  • 遗忘清理的计时器

程序中我们经常会用到计时器,也就是setIntervalsetTimeout

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)微队列:保存待执行的微任务

【JavaScript】50个知识点由浅入深掌握Javascript🔥🔥

【JavaScript】50个知识点由浅入深掌握Javascript🔥🔥

47. 简单介绍一下JS的垃圾回收机制

每隔一段时间,JS的垃圾收集器就会对变量做“巡检”。当它判断一个变量不再被需要之后,它就会把这个变量所占的内存空间给释放掉,这个过程叫做垃圾回收。
常用的垃圾回收算法有两种——引用计数法和标记清除法。

  • 引用计数法

这是最初级的垃圾回收算法,在现代浏览器里几乎被淘汰的干干净净。

当我们创建一个变量,对应的也就创建了一个针对这个值的引用。

const students = ['小红','小明']

在引用这块计数法的机制下,内存中每一个值都会对应一个引用计数。当垃圾收集器感知到某个值的引用计数为0时,就判断它“没用”了,随即这块内存就会被释放。

【JavaScript】50个知识点由浅入深掌握Javascript🔥🔥

比如我们此时如果把student指向一个null:

students = null

那么这个数组所应用的引用计数就会变成0(如下图),它就变成一块没用的内存,即将面临着作为垃圾,被回收的命运。

【JavaScript】50个知识点由浅入深掌握Javascript🔥🔥

引用计数法弊端

大家现在来看这样一个例子:

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)(如下图)。

【JavaScript】50个知识点由浅入深掌握Javascript🔥🔥

这就是引用计数法的弊端,无法甄别循环引用场景下的“垃圾”。

  • 标记清除法

引用计数法无法甄别“循环引用”场景下的“垃圾”,自 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字符串转化为对象。

不足:忽略对象中的函数、undefiendRegExpDate

对象中的函数、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怎么办?是不是默认给他指到windowglobal上去;第一个参数不是对象怎么办?我们改如何保证为对象?如果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,那么实现applybind方法就小菜一碟了,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() // 张三 李四

原文链接:https://juejin.cn/post/7216587974008111164 作者:DarkHorse

(0)
上一篇 2023年3月31日 下午4:16
下一篇 2023年3月31日 下午4:27

相关推荐

发表回复

登录后才能评论