栈和堆的区别
栈(stack):
栈是内存的简称,栈是自动分配相对固定大小的内存空间,并由系统自动释放,栈数据结构遵循FILO(first in last out)先进后出的原则。
堆(heap):
是堆内存的简称,堆是动态分配内存,内存大小不固定,也不会自动释放,堆数据结构是一种无序的树状结构,同时它还满足key-value键值对的存储方式;我们只用知道key名,就能通过key查找到对应的value。比较经典的就是书架存书的例子,我们知道书名,就可以找到对应的书籍;
栈的特点:开口向上、速度快,容量小;
堆的特点:速度稍慢、容量比较大;
JS 中的 8 种数据类型及区别
包括值类型(基本对象类型)和引用类型(复杂对象类型)
基本类型(值类型):
Number(数字),String(字符串),Boolean(布尔),Symbol(符号),null(空),undefined(未定义)
基本类型在内存中占据固定大小,保存在栈内存中。并由系统自动分配和自动释放。这样带来的好处就是,内存可以及时得到回收,相对于堆来说,更加容易管理内存空间。ES6新增:BigInt和Symbol
引用类型(复杂数据类型):
Object(对象)、Function(函数)。其他还有 Array(数组)、Date(日期)、RegExp(正则表达式)、特殊的基本包装类型(String、Number、Boolean) 以及单体内置对象(Global、Math)等
引用类型的值是对象 保存在堆内存中,栈内存存储的是对象的变量标识符以及对象在堆内存中的存储地址。
使用场景:
Symbol:使用 Symbol 来作为对象属性名(key) 利用该特性,把一些不需要对外操作和访问的属性使用 Symbol 来定义
BigInt:由于在 Number 与 BigInt 之间进行转换会损失精度,因而建议仅在值可能大于 253 时使用 BigInt 类型,并且不在两种类型之间进行相互转换。
ES6新特性
- 声明
- const
- let
- 解构赋值
- 模板字符串
- 箭头函数
- promise
- set、map结构
- class类
- for…of,for…of
传送门 ☞ES6全部特性
JS 中的数据类型检测方案
1.typeof
console.log(typeof 1); // number
console.log(typeof true); // boolean
console.log(typeof "mc"); // string
console.log(typeof Symbol); // function
console.log(typeof function () {}); // function
console.log(typeof console.log()); // function
console.log(typeof []); // object
console.log(typeof {}); // object
console.log(typeof null); // object
console.log(typeof undefined); // undefined
优点:能够快速区分基本数据类型
缺点:不能将 Object、Array 和 Null 区分,都返回 object
2.instanceof
instanceof 的作用:
用于判断一个引用类型是否属于某构造函数;还可以在继承关系中用来判断一个实例是否属于它的父类型。
console.log(1 instanceof Number); // false
console.log(true instanceof Boolean); // false
console.log("str" instanceof String); // false
console.log([] instanceof Array); // true
console.log(function () {} instanceof Function); // true
console.log({} instanceof Object); // true
优点:能够区分 Array、Object 和 Function,适合用于判断自定义的类实例对象
缺点:Number,Boolean,String 基本数据类型不能判断
instanceof 和 typeof 的区别
typeof 在对值类型 number、string、boolean 、null 、 undefined、 以及引用类型的 function 的反应是精准的;但是,对于对象{ } 、数组[ ] 、null 都会返回 object
为了弥补这一点,instanceof 从原型的角度,来判断某引用属于哪个构造函数,从而判定它的数据类型。
3.Object.prototype.toString.call() (最优方案)
var toString = Object.prototype.toString;
console.log(toString.call(1)); //[object Number]
console.log(toString.call(true)); //[object Boolean]
console.log(toString.call("mc")); //[object String]
console.log(toString.call([])); //[object Array]
console.log(toString.call({})); //[object Object]
console.log(toString.call(function () {})); //[object Function]
console.log(toString.call(undefined)); //[object Undefined]
console.log(toString.call(null)); //[object Null]
优点:精准判断数据类型
缺点:写法繁琐不容易记,推荐进行封装后使用
原理:
toString()
方法会返回一个表示该对象的字符串Object.prototype
获取我们对象的原型。.call
是为了改变toString函数内部的this指向,同时也防止toString()
方法被重写
传送门 ☞# 如何对JS 中的数据类型进行检测性
var && let && const
ES6 之前创建变量用的是 var,之后创建变量用的是 let/const
三者区别:
ES6之前创建变量用的是var,之后创建变量用的是let/const
三者区别:
重复声明 | 声明提升 | 块级作用域 | 修改值 | |
Var | 可以(后面会覆盖前面的) | 会 | 不可以 | 可以 |
Let(未定义就使用会报错) | 不可以 | 不会 | 可以(只在代码块中执行) | 可以 |
Const(必须初始化、未定义就使用会报错) | 不可以 | 不会 | 可以 | 不可以 |
- var 定义的变量,
没有块的概念,可以跨块访问
, 不能跨函数访问。
let 定义的变量,只能在块作用域里访问,不能跨块访问,也不能跨函数访问。
const 用来定义常量,使用时必须初始化(即必须赋值),只能在块作用域里访问,且不能修改。 - var 可以
先使用,后声明
,因为存在变量提升;let 必须先声明后使用。 - var 是允许在相同作用域内
重复声明同一个变量
的,而 let 与 const 不允许这一现象。 - 在全局上下文中,基于 let 声明的全局变量和全局对象 GO(window)没有任何关系 ;
var 声明的变量会和 GO 有映射关系; 会产生暂时性死区
:
暂时性死区是浏览器的 bug:检测一个未被声明的变量类型时,不会报错,会返回 undefined
如:console.log(typeof a) //undefined\而:let a;console.log(typeof a)//未声明之前不能使用\
- let /const/function 会把当前所在的大括号(除函数之外)作为一个全新的块级上下文,应用这个机制,在开发项目的时候,遇到循环事件绑定等类似的需求,无需再自己构建闭包来存储,只要基于 let 的块作用特征即可解决
作用域和作用域链
定义:简单来说作用域就是变量与函数的可访问范围,由当前环境与上层环境的一系列变量对象组成
创建函数的时候,已经声明了当前函数的作用域==>当前创建函数所处的上下文
。如果是在全局下创建的函数就是[[scope]]:EC(G)
,函数执行的时候,形成一个全新的私有上下文EC(FN)
,供字符串代码执行(进栈执行)
1.全局作用域:代码在程序的任何地方都能被访问,window 对象的内置属性都拥有全局作用域。
2.函数作用域:在固定的代码片段才能被访问
作用:作用域最大的用处就是隔离变量
,不同作用域下同名变量不会有冲突。
作用域链参考链接一般情况下,变量到 创建该变量 的函数的作用域中取值。但是如果在当前作用域中没有查到,就会向上级作用域去查,直到查到全局作用域,这么一个查找过程形成的链条就叫做作用域链。
闭包的两大作用:保存/保护
-
闭包的概念
函数执行时形成的私有上下文 EC(FN),正常情况下,代码执行完会出栈后释放;但是特殊情况下,如果当前私有上下文中的某个东西被上下文以外的事物占用了,则上下文不会出栈释放,从而形成不销毁的上下文。 函数执行函数执行过程中,会形成一个全新的私有上下文,可能会被释放,可能不会被释放,不论释放与否,他的作用是:
(1)保护:划分一个独立的代码执行区域,在这个区域中有自己私有变量存储的空间,保护自己的私有变量不受外界干扰(操作自己的私有变量和外界没有关系);
(2)保存:如果当前上下文不被释放【只要上下文中的某个东西被外部占用即可】,则存储的这些私有变量也不会被释放,可以供其下级上下文中调取使用,相当于把一些值保存起来了;(浏览器的垃圾回收机制:标记-清除法)
我们把函数执行形成私有上下文,来保护和保存私有变量机制称为闭包
。
闭包是指有权访问另一个函数作用域中的变量的函数–《JavaScript 高级程序设计》
稍全面的回答: 在 js 中变量的作用域属于函数作用域, 在函数执行完后,作用域就会被清理,内存也会随之被回收,但是由于闭包函数是建立在函数内部的子函数, 由于其可访问上级作用域,即使上级函数执行完, 作用域也不会随之销毁, 这时的子函数(也就是闭包),便拥有了访问上级作用域中变量的权限,即使上级函数执行完后作用域内的值也不会被销毁。
-
闭包的特性:
-
1、内部函数可以访问定义他们外部函数的参数和变量。(作用域链的向上查找,把外围的作用域中的变量值存储在内存中而不是在函数调用完毕后销毁)设计私有的方法和变量,避免全局变量的污染。
1.1.闭包是密闭的容器,,类似于set、map容器,存储数据的 1.2.闭包是一个对象,存放数据的格式为 key-value 形式
-
2、函数嵌套函数
-
3、本质是将函数内部和外部连接起来。优点是可以读取函数内部的变量,让这些变量的值始终保存在内存中,不会在函数被调用之后自动清除
-
-
闭包形成的条件:
- 函数的嵌套
- 内部函数引用外部函数的局部变量,延长外部函数的变量生命周期
-
闭包的用途:
- 模仿块级作用域
- 保护外部函数的变量 能够访问函数定义时所在的词法作用域(阻止其被回收)
- 封装私有化变量
- 创建模块
-
闭包应用场景
闭包的两个场景,闭包的两大作用:
保存/保护
。 在开发中, 其实我们随处可见闭包的身影, 大部分前端 JavaScript 代码都是“事件驱动”的,即一个事件绑定的回调方法; 发送 ajax 请求成功|失败的回调;setTimeout 的延时回调;**或者一个函数内部返回另一个匿名函数,这些都是闭包的应用。 -
闭包的优点:延长局部变量的生命周期
-
闭包缺点:会导致函数的变量一直保存在内存中,过多的闭包可能会导致内存泄漏
JS 中 this 的情况
- 普通函数调用:通过函数名()直接调用:
this
指向全局对象window
(注意 let 定义的变量不是 window 属性,只有 window.xxx 定义的才是。即 let a =’aaa’; this.a 是 undefined) - 构造函数调用:函数作为构造函数,用 new 关键字调用时:
this
指向新new出的对象
- 对象函数调用:通过对象.函数名()调用的:
this
指向这个对象
- 箭头函数调用:箭头函数里面没有 this ,所以
永远是上层作用域this
(上下文) - apply 和 call 调用:函数体内 this 的指向的是 call/apply 方法
第一个参数
,若为空默认是指向全局对象 window。 - 函数作为数组的一个元素,通过数组下标调用的:this 指向这个数组
- 函数作为 window 内置函数的回调函数调用:this 指向 window(如 setInterval setTimeout 等)
this调用规则优先级:
- 默认规则的优先级最低
- 显示绑定优先级高于隐式绑定
- new绑定优先级高于隐式绑定
- new绑定优先级高于bind
- new绑定和call、apply是不允许同时使用的,所以不存在谁的优先级更高
优先级总结:
new绑定 > 显示绑定(bind)> 隐式绑定 > 默认绑定**
call/apply/bind 的区别
参数 | 执行 | |
---|---|---|
apply | 数组(必须一次性传入) | 立即执行 |
call | 参数列表 | 立即执行 |
bind | 参数列表(可以分多次传入) | 不会立即执行,返回一个永久改变this指向的函数。 |
总结:基于 Function.prototype 上的 apply 、 call 和 bind
调用模式,这三个方法都可以显示的指定调用函数的 this 指向。apply
接收参数的是数组,call
接受参数列表,`` bind方法通过传入一个对象,返回一个
this绑定了传入对象的新函数。这个函数的
this指向除了使用
new `时会被改变,其他情况下都不会改变。若为空默认是指向全局对象 window。
参考:☞ # 关于JavaScript中apply、call、bind的详细使用讲解
箭头函数的特性
箭头函数没有自己的this
,会捕获其所在的上下文的 this 值,作为自己的 this 值箭头函数没有constructor
,是匿名函数,不能作为构造函数,不能通过 new 调用;没有new.target 属性
。在通过 new 运算符被初始化的函数或构造方法中,new.target 返回一个指向构造方法或函数的引用。在普通的函数调用中,new.target 的值是 undefined箭头函数不绑定Arguments 对象
。取而代之用 rest 参数…解决。由于 箭头函数没有自己的 this 指针,通过 call() 或 apply() 方法调用一个函数时,只能传递参数(不能绑定 this),他们的第一个参数会被忽略。(这种现象对于 bind 方法同样成立)- 箭头函数通过 call() 或 apply() 方法调用一个函数时,只传入了一个参数,对 this 并没有影响。
- 箭头函数没有原型属性 Fn.prototype 值为 undefined
- 箭头函数不能当做 Generator 函数,不能使用 yield 关键字
参考:# 箭头函数的详细讲解
原型 && 原型链
原型关系:
- 每个 class 都有显示原型 prototype
- 每个实例都有隐式原型
__proto__
- 实例的
__proto__
指向对应 class 的 prototype
原型: 在 JS 中,每当定义一个对象(函数也是对象)时,对象中都会包含一些预定义的属性。其中每个函数对象
都有一个prototype
属性,这个属性指向函数的原型对象
。
原型链:函数的原型链对象 constructor 默认指向函数本身,原型对象除了有原型属性外,为了实现继承,还有一个原型链指针proto,该指针是指向上一层的原型对象,而上一层的原型对象的结构依然类似。因此可以利用proto一直指向 Object 的原型对象上,而 Object 原型对象用 Object.prototype.proto = null 表示原型链顶端。如此形成了 js 的原型链继承。同时所有的 js 对象都有 Object 的基本防范
特点: JavaScript
对象是通过引用来传递的,我们创建的每个新对象实体中并没有一份属于自己的原型副本。当我们修改原型时,与之相关的对象也会继承这一改变。
new 运算符的实现机制
- 首先创建了一个新的
空对象
设置原型
,将对象的原型设置为函数的prototype
对象。- 让函数的
this
指向这个对象,执行构造函数的代码(为这个新对象添加属性) - 判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。
EventLoop 事件循环
JS
是单线程的,为了防止一个函数执行时间过长阻塞后面的代码,所以会先将同步代码压入执行栈中,依次执行,将异步代码推入异步队列,异步队列又分为宏任务队列和微任务队列,因为宏任务队列的执行时间较长,所以微任务队列要优先于宏任务队列。微任务队列的代表就是,Promise.then
,MutationObserver
,宏任务的话就是setImmediate setTimeout setInterval
JS 运行的环境。一般为浏览器或者 Node。 在浏览器环境中,有 JS 引擎线程和渲染线程,且两个线程互斥。 Node 环境中,只有 JS 线程。 不同环境执行机制有差异,不同任务进入不同 Event Queue 队列。 当主程结束,先执行准备好微任务,然后再执行准备好的宏任务,一个轮询结束。
浏览器中的事件环(Event Loop)
事件环的运行机制是,先会执行栈中的内容,栈中的内容执行后执行微任务,微任务清空后再执行宏任务,先取出一个宏任务,再去执行微任务,然后在取宏任务清微任务这样不停的循环。
-
eventLoop 是由 JS 的宿主环境(浏览器)来实现的;
-
事件循环可以简单的描述为以下四个步骤:
- 函数入栈,当 Stack 中执行到异步任务的时候,就将他丢给 WebAPIs,接着执行同步任务,直到 Stack 为空;
- 此期间 WebAPIs 完成这个事件,把回调函数放入队列中等待执行(微任务放到微任务队列,宏任务放到宏任务队列)
- 执行栈为空时,Event Loop 把微任务队列执行清空;
- 微任务队列清空后,进入宏任务队列,取队列的第一项任务放入 Stack(栈)中执行,执行完成后,查看微任务队列是否有任务,有的话,清空微任务队列。重复 4,继续从宏任务中取任务执行,执行完成之后,继续清空微任务,如此反复循环,直至清空所有的任务。
-
浏览器中的任务源(task):
宏任务(macrotask)
:
宿主环境提供的,比如浏览器
ajax、setTimeout、setInterval、setTmmediate(只兼容 ie)、script、requestAnimationFrame、messageChannel、UI 渲染、一些浏览器 api微任务(microtask)
:
语言本身提供的,比如 promise.then
then、queueMicrotask(基于 then)、mutationObserver(浏览器提供)、messageChannel 、mutationObersve
Node 环境中的事件环(Event Loop)
Node
是基于 V8 引擎的运行在服务端的JavaScript
运行环境,在处理高并发、I/O 密集(文件操作、网络操作、数据库操作等)场景有明显的优势。虽然用到也是 V8 引擎,但由于服务目的和环境不同,导致了它的 API 与原生 JS 有些区别,其 Event Loop 还要处理一些 I/O,比如新的网络连接等,所以 Node 的 Event Loop(事件环机制)与浏览器的是不太一样。
执行顺序如下:
timers
: 计时器,执行 setTimeout 和 setInterval 的回调pending callbacks
: 执行延迟到下一个循环迭代的 I/O 回调idle, prepare
: 队列的移动,仅系统内部使用poll轮询
: 检索新的 I/O 事件;执行与 I/O 相关的回调。事实上除了其他几个阶段处理的事情,其他几乎所有的异步都在这个阶段处理。check
: 执行setImmediate
回调,setImmediate 在这里执行close callbacks
: 执行close
事件的callback
,一些关闭的回调函数,如:socket.on(‘close’, …)
传送门 ☞ 说说事件循环机制(满分答案来了)
setTimeout、Promise、Async/Await 的区别
-
setTimeout
settimeout 的回调函数放到宏任务队列里,等到执行栈清空以后执行。
-
Promise
Promise 本身是同步的立即执行函数, 当在 executor 中执行 resolve 或者 reject 的时候, 此时是异步操作, 会先执行 then/catch 等,当主栈完成后,才会去调用 resolve/reject 中存放的方法执行。
console.log("script start"); let promise1 = new Promise(function (resolve) { console.log("promise1"); resolve(); console.log("promise1 end"); }).then(function () { console.log("promise2"); }); setTimeout(function () { console.log("settimeout"); }); console.log("script end"); // 输出顺序: script start->promise1->promise1 end->script end->promise2->settimeout
-
async/await
async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async 函数体。
async function async1() { console.log("async1 start"); await async2(); console.log("async1 end"); } async function async2() { console.log("async2"); } console.log("script start"); async1(); console.log("script end"); // 输出顺序:script start->async1 start->async2->script end->async1 end
传送门 ☞ ## 看完这个,还不会Promise,请你吃瓜 !!!
Async/Await 如何通过同步的方式实现异步
Async/Await 就是一个自执行的 generate 函数。利用 generate 函数的特性把异步的代码写成“同步”的形式,第一个请求的返回值作为后面一个请求的参数,其中每一个参数都是一个 promise 对象.
介绍节流防抖原理、区别以及应用
节流
:事件触发后,规定时间内,事件处理函数不能再次被调用。也就是说在规定的时间内,函数只能被调用一次,且是最先被触发调用的那次。
防抖
:多次触发事件,事件处理函数只能执行一次,并且是在触发操作结束时执行。也就是说,当一个事件被触发准备执行事件函数前,会等待一定的时间(这时间是码农自己去定义的,比如 1 秒),如果没有再次被触发,那么就执行,如果被触发了,那就本次作废,重新从新触发的时间开始计算,并再次等待 1 秒,直到能最终执行!
使用场景
:
节流:滚动加载更多、搜索框搜的索联想功能、高频点击、表单重复提交……
防抖:搜索框搜索输入,并在输入完以后自动搜索、手机号,邮箱验证输入检测、窗口大小 resize 变化后,再重新渲染。
/**
* 节流函数 一个函数执行一次后,只有大于设定的执行周期才会执行第二次。有个需要频繁触发的函数,出于优化性能的角度,在规定时间内,只让函数触发的第一次生效,后面的不生效。
* @param fn要被节流的函数
* @param delay规定的时间
*/
function throttle(fn, delay) {
//记录上一次函数触发的时间
var lastTime = 0;
return function () {
//记录当前函数触发的时间
var nowTime = Date.now();
if (nowTime - lastTime > delay) {
//修正this指向问题
fn.call(this);
//同步执行结束时间
lastTime = nowTime;
}
};
}
document.onscroll = throttle(function () {
console.log("scllor事件被触发了" + Date.now());
}, 200);
/**
* 防抖函数 一个需要频繁触发的函数,在规定时间内,只让最后一次生效,前面的不生效
* @param fn要被节流的函数
* @param delay规定的时间
*/
function debounce(fn, delay) {
//记录上一次的延时器
var timer = null;
return function () {
//清除上一次的演示器
clearTimeout(timer);
//重新设置新的延时器
timer = setTimeout(function () {
//修正this指向问题
fn.apply(this);
}, delay);
};
}
document.getElementById("btn").onclick = debounce(function () {
console.log("按钮被点击了" + Date.now());
}, 1000);
如何实现去重
- 利用 ES6 的Array.from和 Set 结合去重
var arr = [1, 1, 8, 8, 12, 12, 15, 15, 16, 16];
function unique(arr) {
return Array.from(new Set(arr));
}
console.log(unique(arr));
//[1,8,12,15,16]
仅用set去重
function unique(arr) {
if (!Array.isArray(arr)) {
console.log('type error!')
return
}
return [...new Set(arr)]
}
优点:
- 代码量少
缺点:
- 兼容性差
- 无法去重引用类型的数据
对象数组去重
function uniqueFunc(arr, uniId){
const res = new Map();
return arr.filter((item) => !res.has(item[uniId]) && res.set(item[uniId], 1));
}
事件委托
定义借助事件冒泡的原理,将元素的事件委托给它的父级或者更外级的元素处理
- 优点:
- 减少内存消耗
- 提高JavaScript性能
ES6的Set和Map
set
- 集合是由一组无序且唯一(即不能重复)的项组成的,可以想象成集合是一个既没有重复元素,也没有顺序概念的数组
- ES6提供了新的数据结构Set。它类似于数组,但是成员的值都是唯一的,没有重复的值
- Set 本身是一个构造函数,用来生成 Set 数据结构
- 这里说的Set其实就是我们所要讲到的集合,先来看下基础用法
- Set的属性:
- size:返回集合所包含元素的数量
- Set的方法:
- 操作方法
- add(value):向集合添加一个新的项
- delete(value):从集合中移除一个值
- has(value):如果值在集合中存在,返回true,否则false
- clear(): 移除集合里所有的项
- 遍历方法
- keys():返回一个包含集合中所有键的数组
- values():返回一个包含集合中所有值的数组
- entries:返回一个包含集合中所有键值对的数组(感觉没什么用就不实现了)
- forEach():用于对集合成员执行某种操作,没有返回值
Map
那么集合又和字典有什么区别呢:
- 共同点:集合、字典可以存储不重复的值
- 不同点:集合是以[值,值]的形式存储元素,字典是以[键,值]的形式存储
属性:
- size:返回字典所包含的元素个数
操作方法:
- set(key, val): 向字典中添加新元素
- get(key):通过键值查找特定的数值并返回
- has(key):如果键存在字典中返回true,否则false
- delete(key): 通过键值从字典中移除对应的数据
- clear():将这个字典中的所有元素删除
遍历方法:
- keys():将字典中包含的所有键名以数组形式返回
- values():将字典中包含的所有数值以数组形式返回
- forEach():遍历字典的所有成员
Set和Map的区别
- Map是键值对,Set是值得集合,当然键和值可以是任何得值
- Map可以通过get方法获取值,而set不能因为它只有值
- 都能通过迭代器进行for…of 遍历
- Set的值是唯一的可以做数组去重,而Map由于没有格式限制,可以做数据存储