50个知识点由浅入深掌握Javascript

news2024/11/20 9:46:24

前言

近期整理了JavaScript知识体系,50个知识点由浅入深掌握Js建议收藏,如有问题,欢迎指正。

1. 说说你对JS的理解

1995年,布莱登·艾奇(美国人)在网景公司,用10天写的一门语言。
Js是一门:动态的,弱类型的,解释型的,基于对象的脚本语言,同时Js又是单线程的。

  • 动态类型语言:
    代码在执行过程中,才知道这个变量属于的类型。
  • 弱类型:声明变量一般用var,数据类型不固定,可以随时改变。可以将字符串’12’和整数3进行连接得到字符串’123’,在相加的时候会进行强制类型转换。
  • 解释型:一边执行,一边编译,不需要程序在运行之前需要整体先编译。
  • 基于对象:最终所有对象都指向Object
  • 脚本语言 :一般都是可以嵌在其它编程语言当中执行。
  • 单线程:依次执行,前面代码执行完后面才执行。

组成部分:

ECMAscriptDOMBOM
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

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

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

QQ截图20230327113517副本.png

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等语言采纳的是动态作用域

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

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()查询并返回指定子串的索引,不存在返回-1n
lastIndexOf()反向查询并返回指定子串的索引,不存在返回-1n
localeCompare()比较原串和指定字符串:原串大返回1,原串小返回-1,相等返回0n
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 对象的子对象。

image.png

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

src=http___images2015.cnblogs.com_blog_740839_201702_740839-20170210181415635-217958893.png&refer=http___images2015.cnblogs.webp

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

image.png

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

也就是说当事件在层层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

在上述代码中,通过自定义 setget 函数的方式,在原本的逻辑中插入了我们的函数逻辑,实现了在对对象任何属性进行读写时发出通知。

当然这是简单版的响应式实现,如果需要实现一个 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

原型就相当于一个公共区域,可以被类和该类的所有实例访问到。
在这里插入图片描述

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

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

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

原型链:实际上是指隐式原型链,从对象的__proto__开始,连接所有的对象,就是对象查找属性或方法的过程。
隐式原型链

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

image.png

image.png

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

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

  • 引用计数法

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

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

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

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

image.png

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

students = null

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

image.png

引用计数法弊端

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

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

image.png

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

  • 标记清除法

引用计数法无法甄别“循环引用”场景下的“垃圾”,自 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() // 张三 李四

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/699321.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【ArcGIS】使用ArcMap进行北京1954-120E坐标转WGS84坐标系

背景 在进行青岛地市GIS数据迁移&#xff0c;涉及坐标转换&#xff0c;经过几天摸索终于找到迁移方法 投影坐标系 北京1954-120E坐标 对应为高斯-克吕格投影 300000 3000001 0 0&#xff08;青岛本地坐标&#xff09; 增量:-300000 -3000001&#xff08;此处为示例&#xff0c…

Python print()函数使用详解,Python打印输出

「作者主页」&#xff1a;士别三日wyx 「作者简介」&#xff1a;CSDN top100、阿里云博客专家、华为云享专家、网络安全领域优质创作者 「推荐专栏」&#xff1a;对网络安全感兴趣的小伙伴可以关注专栏《网络安全入门到精通》 print() 可以「打印输出」&#xff0c;常用来将内…

ICV报告:2023年全球量子信息上市企业第一季度报告

ICV分析师在报告中所认定的“上市”&#xff0c;指的是公司公开发行股票&#xff08;例如IPO&#xff09;、公司在交易所挂牌交易、公司以SPAC&#xff08;特殊目的收购公司&#xff09;等形式进入公开交易市场&#xff0c;实现公司资本化并披露公司信息的情况。 报告研究的“…

预约时间列表

/*** 时间列表* $interval 间隔X分钟* */ function timeList($day7,$time108:00,$time222:00,$interval60){$date_list [];//日期列表$today_date strtotime(date(Y-m-d,time()));for($i0;$i<$day;$i){$date_title date(Y-m-d,$today_date($i*86400));$buff array();for…

MATLAB App Designer基础教程 Matlab GUI入门(二)

MATLAB GUI入门 第二天 —— Lamp (灯)霓虹灯控件的使用 一、主要内容: 技巧 1.Tooltip的使用 2.Vislble和Enable 3.lf函数语句的使用需求&#xff1a;根据阈值进行提示 1.红色温度过高>500 ⒉橙色温度适中400~500 3.蓝色温度过低<400 二、项目背景: &#xff08;案例…

简单的手机记事本哪个好用?

在快节奏的现代生活中&#xff0c;我们经常需要记录下来重要的信息&#xff0c;而手机记事本成为了不可或缺的工具。然而&#xff0c;市面上琳琅满目的手机记事本软件&#xff0c;让人眼花缭乱&#xff0c;不知道该选择哪一个。 敬业签是功能强大、操作简单的手机记事本&#…

最新,2023年6月CDGP设计及论述题解析

2023年6月CDGP设计及论述题解析 &#xff08;加gzh“大数据食铁兽”&#xff0c;回复“2023cdgp”获取完整版&#xff09; 酒店会员建模 结合国内外数据安全法律法规&#xff0c;谈谈境外传输数据安全管理体系建设 国内&#xff1a;《数据安全法》、《网络安全法》、2022年9月…

【juc】原子数组

目录 一、代码示例二、示例截图 一、代码示例 package com.learning.atomic;import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.atomic.AtomicIntegerArray; import java.util.function.BiConsumer; import java.uti…

Qt-解决异常报错“QAxBase::setControl: requested control XXX could not be instantiated”

作者&#xff1a;翟天保Steven 版权声明&#xff1a;著作权归作者所有&#xff0c;商业转载请联系作者获得授权&#xff0c;非商业转载请注明出处 问题说明 使用Qt开发的过程中&#xff0c;QAxObject是经常用到的一个类&#xff0c;用于操作Windows中各种COM接口&#xff0c;进…

什么是开源工作流系统?内容涉及哪些方面?

随着低代码开发市场的繁荣发展&#xff0c;它的灵活、简便、易操作、好维护等优势特点深得广大用户朋友的喜爱&#xff0c;是推动其走向流程化管理的重要推动力。那么&#xff0c;您了解开源工作流系统吗&#xff1f;知道它都有哪些主要内容吗&#xff1f;如果想了解这方面的内…

浅析金鸣识别所用的Canny边缘检测算法和Sobel算子

Canny边缘检测算法和Sobel算子都是金鸣识别常用的图像边缘检测算法&#xff0c;在识别图片表格过程中金鸣识别通常会根据不同的场景混合使用它们&#xff0c;以达到最佳的识别效果&#xff0c;它们是非常先进的算法&#xff0c;下面我们来看看它们的实现步骤与区别。 Canny边缘…

多智能体强化学习理论与算法总结

多智能体强化学习理论与算法总结 先搞明白on-policy和off-policy 【强化学习】一文读懂&#xff0c;on-policy和off-policy 我的理解&#xff1a;on-policy就是使用最新的策略来执行动作收集数据&#xff0c;off-policy的训练数据不是最新策略收集的。on-policy也是使用同个策…

苹果手机ios设备管理软件iMazing 2.17.6官方版下载及常见问题解决

苹果手机ios设备管理软件iMazing 2.17.6官方版下载(ios设备管理软件)是一款管理苹果设备的软件&#xff0c; Windows / macos 系统上的一款帮助用户管理 IOS 手机的应用程序&#xff0c;软件功能非常强大&#xff0c;界面简洁明晰、操作方便快捷&#xff0c;设计得非常人性化。…

electron+vue3+ts+vite

首先使用vite工具创建一个vue3ts的项目 npm create vite创建好vuets项目后启动项目 cd electron-vue3-ts-vitenpm installnpm run dev 访问http://127.0.0.1:5173/地址可以看到项目已经启动成功 安装Electron 接下来我们安装electron&#xff0c;使用以下命令 npm i -D el…

FlashAttention论文解析

FlashAttention让语言模型拥有更长的上下文 FlashAttention序&#xff1a;概述&#xff1a;简介&#xff1a;FlashAttention块稀疏 FlashAttention优点&#xff1a;标准注意力算法实现流程&#xff1a; FlashAttentionBlock-Sparse FlashAttention实验使用FlashAttention后更快…

【网络管理发展】网络杂谈(12)之网络管理未来发展趋势

涉及知识点 网络管理未来的发展方向&#xff0c;网络管理未来的发展趋势&#xff0c;个人闲谈网络管理未来发展&#xff0c;网络管理技术现状&#xff0c;应用服务供应商&#xff08;ASP&#xff09;&#xff0c;网络的远程管理&#xff0c;人工智能与未来。 原创于&#xff1…

try catch 异常处理

C中使用异常时应注意的问题任何事情都是两面性的&#xff0c;异常有好处就有坏处。如果你是C程序员&#xff0c; 并且希望在你的代码中使用异常&#xff0c;那么下面的问题是你要注意的。1. 性能问题。这个一般不会成为瓶颈&#xff0c;但是如果你编写的是高性能或者实时性要求…

保偏产品系列丨5款保偏光纤产品简介

保偏光纤应用日益扩大&#xff0c;特别是在干涉型传感器等测量方面&#xff0c;利用保偏光纤的光无源器件起着非常重要的作用&#xff0c;种类也很多。 本文来介绍5款保偏光纤系列产品以及它们的性能&#xff0c;欢迎收藏转发哦&#xff01; 01、保偏光纤跳线-TLPMPC 保偏光纤跳…

2015年全国硕士研究生入学统一考试管理类专业学位联考数学试题——纯题目版

2015 级考研管理类联考数学真题 一、问题求解&#xff08;本大题共 15 小题&#xff0c;每小题 3 分&#xff0c;共 45 分&#xff09;下列每题给出 5 个选项中&#xff0c;只有一个是符合要求的&#xff0c;请在答题卡上将所选择的字母涂黑。 1.若实数a,b, c 满足 a : b : c…

手机记事本中的内容转到新手机不见了,怎么办?

在更换新手机时&#xff0c;很多网友都会面临这样一个问题&#xff0c;这就是旧手机中的重要数据如何转移到新手机上。一般来说&#xff0c;如果是相同品牌的手机&#xff0c;我们可以借助手机云空间账号进行数据的同步&#xff1b;但如果使用的是不同品牌的手机&#xff0c;这…