1、执行上下文
执行上下文
-
执行上下文是评估和执行 JavaScript 代码的环境的抽象概念。每当 Javascript 代码在运行的时候,它都是在执行上下文中运行。
-
首先,执行上下文分为三种:全局、函数、eval函数(计算字符串表达式)
- 匿名函数是在全局上下文中执行的
-
它们都是由栈这一数据结构,也就是执行上下文栈实现的;对于函数,调用时创建,执行完一个就弹出,对于全局是整个应用程序结束时才会被清空
-
创建执行上下文的过程分为两个阶段
- 创建阶段
- this值的确定,全局中是全局对象windows,函数则取决是被谁调用(谁调用就指向谁,引用对象/全局对象/undefined)
- 词法环境,也就是标识符与变量引用的映射(let、const)
- 环境记录器:存储变量与函数声明的实际位置
- 外部环境的应用:也就是作用域
- 变量环境,也算是一个词法环境,只var变量绑定
- 执行阶段:执行代码
- 变量赋值
- 函数引用
- 执行其他代码
- 创建阶段
闭包、及其应用场景
- 闭包定义:闭包就是内部函数,也就是函数内部或者{}中定义一个函数来创建闭包
- 外部函数作用域:内部函数可以访问外部函数中定义的变量,即使外部函数已经执行完毕。(因为引用外部变量,闭包的真正威力:执行完毕仍可调用、信息隐藏也就是私有状态的函数、避免全局变量)
- 外部块作用域:内部函数可访问外部块的
- 词法作用域:外部作用域。
- 作用域链:每一个作用域都有对其父作用域的引用。当我们使用一个变量的时候,
Javascript引擎
会通过变量名在当前作用域查找,若没有查找到,会一直沿着作用域链一直向上查找,直到global
全局作用域。 - 造成内存泄漏,因此要将外部函数置为null
- 只能通过返回的对象中的方法进行访问。这种模块模式通过闭包创建了一个私有的作用域,保护了内部的变量。
- 每个循环中的回调函数都会捕获到正确的
index
值,输出 0 到 4。 - 函数执行会形成一个全新的私有作用域,保护里面的变量不受外界干扰,这种保护机制就是闭包。
实现数据私有
被封装的变量只能在闭包容器函数作用域中使用。你无法绕过对象被授权的方法在外部访问这些数据。
被保护的变量只能通过暴露出的闭包引用,所使用。
保存数据
setTimeout,for循环中的var,
作用域链条
- 函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中,你可以理解 [[scope]] 就是所有父变量对象的层级链,但是注意:[[scope]] 并不代表完整的作用域链!
2、this/call/apply/bind
介绍js的this
- this是一个执行上下文中的对象的引用,在函数被调用的时候确定引用(也就是在执行上下文的时候)
- \1. 以函数的形式调用时,this永远都是window
\2. 以方法的形式调用时,this就是调用方法的对象
\3. 以构造函数的形式调用时,this就是新创建的对象
\4. 使用call和apply调用时,this就是指定的那个对象
\5. 在全局作用域中this代表window
- \1. 以函数的形式调用时,this永远都是window
- 分类
- 全局对象的this就是其本身,windows
- 函数的this
- 使用call、apply指定this
- 构造函数与原型方法上的this
- new操作符调用构造函数的阶段
- 创建新对象
- 该对象原型指向构造函数的原型
- 将构造函数的this指向这个对象
- 指向构造函数的代码,为其添加属性方法等
- 返回新对象(值类型?值:引用)
- new操作符调用构造函数的阶段
改变this指向
- 箭头函数
- 箭头函数需要记着这句话:“箭头函数中没有 this 绑定,必须通过查找作用域链来决定其值,如果箭头函数被非箭头函数包含,则 this 绑定的是最近一层非箭头函数的 this,否则,this 为 undefined”。
- 函数内部使用_this=this
- 使用apply、call、bind
- new实例化一个对象
call与apply的区别
-
- 调用形式:
- fn.call(obj, param1, param2, …)
fn.apply(obj, [param1,param2,…])
fn.bind(obj, param1, param2, …)
- fn.call(obj, param1, param2, …)
- 定义
- call:
call()
允许为不同的对象分配和调用属于一个对象的函数/方法 - apply
- bind:bind()方法不会立即执行目标函数,而是返回一个原函数的拷贝,并且拥有指定
this
值和初始函数(为什么是指定的,当然是我们自己传进去的啦),参数可以后续调用的时候再传入
- call:
- 调用形式:
-
总体的理解:
- call、apply 和 bind 是挂在 Function 对象上的三个方法,所以调用这三个方法的必须是一个函数。
- JavaScript内部提供了一种机制,让我们可以自行手动设置this的指向。它们就是call与apply。它们除了参数略有不同之外,其功能完全一样。它们的第一个参数都为this将要指向的对象。
- call与applay后面的参数,都是向将要执行的函数传递参数。其中call以一个一个的形式传递,apply以数组的形式传递。这是他们唯一的不同。
- call、apply两者和bind的区别:返回的结果不一样,bind返回的是Function类型。
-
使用场景:
- 根据自己的需要灵活修改this指向
- 将类数组对象转换为数组(类数组对象,也就是类似于数组的对象,比如说字符串、arguments等)
- 实现继承
- 在向其他执行上下文的传递中,确保this的指向保持不变
如何实现call和apply、bind
3、原型/继承
介绍js的原型
- 构造函数的prototype属性,实例对象的____proto____属性(隐式引用,无法直接访问),指向原型对象
- 什么有可能是原型对象:几乎所有对象都有可能,功能有给其它对象提供共享属性的对象,一个对象可以同时是原型对象与实例对象(于是构成原型链)
- 原型对象的constructor指向构造函数
- 构造函数的本质,它其实是在new内部实现的一个复制过程
- 实例对象实际上对前面我们所说的中间对象的复制,而中间对象中的属性与方法都在构造函数中添加。于是根据构造函数与原型的特性,我们就可以将在:
- 构造函数中,通过this声明的属性与方法称为私有变量与方法,它们被当前被某一个实例对象所独有。优先访问
- 而通过原型声明的属性与方法,我们可以称之为共有属性与方法,它们可以被所有的实例对象访问。console.log(p1.getName === p2.getName); // true
原型链是什么
- 原型链如何构成:add是Function对象的实例;而Function的原型对象同时又是Object的实例。这样就构成了一条原型链。
- 原型链的访问,其实跟作用域链有很大的相似之处,他们都是一次单向的查找过程。因此实例对象能够通过原型链,访问到处于原型链上对象的所有属性与方法。这也是foo最终能够访问到处于Object原型对象上的toString方法的原因。
- 在 JavaScript 中,
Object.prototype
是所有对象的原型链的顶端,而它本身的原型是null
。也就是说Obje.prototype._proto=null - 一个易错的例子。
如何利用原型实现继承
- 构造函数的继承:构造函数.call(this,参数1,参数2),然后设置其他属性this.name=name,等等
- 原型继承:让构造函数的prototype指向另一构造函数的实例
- 需要考虑:如何将子类对象的原型加入到原型链中?我们只需要让子类对象的原型,成为父类对象的一个实例,然后通过
__proto__
就可以访问父类对象的原型。 - 六种继承
- 原型链继承:设置构造函数的原型是某原型的**实例**,但引用对象被所有实例共享
- 当原型上的属性是引用数据类型时,所有实例都会共享这个属性,即某个实例对这个属性重写会影响其他实例。
- 盗用构造函数继承:在内部通过call调用父类性的构造函数,但无法很好地复用函数
- 必须在构造函数中定义方法,通过盗用构造函数继承的方法本质上都变成了实例自己的方法,不是公共的方法,因此失去了复用性。
- 组合式继承:使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承.
- 那就是在实现的过程中调用了两次
Person
构造函数
- 那就是在实现的过程中调用了两次
- 原型继承:本质与原型链继承基本一致,Object.create(原型对象,当前对象新增属性),原型式继承非常适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合。
- 如果你有一个已知的对象,想在它的基础上再创建一个新对象(把这个已知对象设为新对象的prototype指向),那么你只需要把已知对象传给
object
函数即可。
- 如果你有一个已知的对象,想在它的基础上再创建一个新对象(把这个已知对象设为新对象的prototype指向),那么你只需要把已知对象传给
- 寄生式继承:是对原型继承的扩展,把一个对象包装到一个函数当中,最后返回这个函数的调用
- 寄生式组合继承:
- 原型链继承:设置构造函数的原型是某原型的**实例**,但引用对象被所有实例共享
- 需要考虑:如何将子类对象的原型加入到原型链中?我们只需要让子类对象的原型,成为父类对象的一个实例,然后通过
function clone(parent,child){
//将子函数的原型指向父函数的原型创建出来的新对象
child.prototype=Object.create(parent.prototype);
//通过上一个,子函数的构造函数也变成了父对象,所以需要重新指向子类
child.prototype.constructor=child;
}
- 属性的属性类型
- 可通过Objec.defineProperty修改属性特性,Object.defineProperty
只能设置一个属性的属性特性。当我们想要同时设置多个属性的特性时,需要使用我们之前提到过的
Object.defineProperties - 读取:Object.getOwnPropertyDescriptor(对象,标识符字符串,包含属性类型的对象)
- 可通过Objec.defineProperty修改属性特性,Object.defineProperty
4、promise
promise是什么:是一个构造函数,一个对象
有关的十道题、手撕promise的各类方法、很难的45道题、
- 为什么有promise:在实际的使用中,有非常多的应用场景我们不能立即知道应该如何继续往下执行。
- 避免回调地狱:多次嵌套回调函数
- 将数据请求与数据处理明确的区分开来。
- 回调函数:只不过这个所谓的回调函数是将要被当做参数传递给另一个函数,并被其调用(区别就在这,一般函数的形参,接收的是一个基本类型的变量,而这个函数,接受的参数居然是一个"函数",这个作为参数的函数,就叫回调函数),你所看到的资源为什么会讲那么复杂,是因为它得告诉你为什么有这样的需求。也就是说它想要给你讲的是回调函数的实用场景。
- promise三种状态:pending等待中,resolved/fulfilled已经完成,rejected得到结果但非期望。resolve()、reject()这两个函数修改pending状态,一旦变了一次之后再调用是无效的
- promise的构造函数:new promise(函数[处理promise状态变化]),里面的回调函数的参数也是函数,分别是resolve、reject,调用时会修改promise状态
- promise的then方法,接受构造函数中处理的状态变化,并分别执行,then(函数[resolved状态执行],函数[reject状态执行])
- 会返回新的promise实例,返回任意一个非 promise 的值都会被包裹成 promise 对象
- promise的数据传递
- 第一个参数会立即执行
- promise的catch方法,指定reject的回调,有错误就运行此回调
- catch与then不能返回其本身
- promise的all方法,全都执行,数据会以数组返回;参数:promise对象数组,所有对象都变成resolved或者rejected时,才会调用then;
- promise的race方法,谁跑得快,就以谁为准执行
如何实现promise
async await
-
await bar()相当于promise.resolve(bar()),前面的是同步代码,在其之后的相当于then方法里;在这里,你可以理解为「紧跟着await后面的语句相当于放到了new Promise中,下一行及之后的语句相当于放在Promise.then中」。
-
有什么用:用同步方式,执行异步操作
-
怎么做:在
async
函数中,await
规定了异步操作只能一个一个排队执行,从而达到用同步方式,执行异步操作的效果,这里注意了:await只能在async函数中使用,不然会报错哦 -
async函数执行完会返回一个状态为resolved的promise;await后面最好跟promise,这样可以实现排队,虽然其他的也可以实现排队;
-
语法糖:改变语法结构使其更易读
-
generator函数
使用yield:只有该函数中才能使用,是generator函数执行的中途暂停点.
yield后接变量,则返回此变量
yield后接函数,则立刻执行,并且该函数执行的返回值,会被当作此暂停点对象的value属性的值{value:res,done:false}
yield后接promise,同上?
暂停之后如何往下走next:使用generator.next()方法
-next函数传参:第一次传参没用,第二次开始才有用;next传值时,要记住顺序是,先右边yield,后左边接收参数
-
5、事件机制/event loop
介绍一个事件的发布订阅
介绍事件循环机制:来自html规范,实现由浏览器或node环境实现,js执行本身只是一部分
-
认识到js的一大特点是:单线程(不过web worker涉及到多线程)
所有有了同步(顺序执行得到预期结果)、异步(无法得到预期结果,需要通过一定手段)的概念,不可能主线程阻塞,就要一直等待它完成
-
js代码执行过程中:1、函数调用栈搞定函数的执行顺序;2、任务队列搞定另外一些代码的执行
-
一个线程中:事件循环是唯一的,任务队列可以拥有多个(宏任务队列、微任务队列,微任务优于宏任务,同步代码执行完才能开始微任务,微任务要执行完执行栈才能继续下一个微任务,微任务执行完毕才能进行下一个宏任务,)
异步任务分为两类
- 宏任务:script(整体代码),setTimeout,setInternal,I/O,UIrendering
- 微任务:process.nextTick,Promise的方法比如then,Object.observe(已废弃),MutationObserver(html5新特性)
-
任务队列中取出一个任务,于是会创建一个执行栈,执行栈的执行根据其是同步还是异步,决定是压入栈立刻执行还是放入任务队列(异步模块)。
宏任务和微任务有什么区别
宏任务代表一个独立的、完整的执行单元,它会在主线程上执行。
6、深浅拷贝
-
为什么有深浅拷贝之分:
-
基本数据类型:存贮在栈,按值存储
Number\String\Boolean\Null\Undefined\Symbol
-
引用数据类型:存贮在堆,存储地址
Object\Array\Date\RegExp\Function
-
浅拷贝:直接赋值,不论是地址还是真实的值
-
深拷贝:开辟新的栈,两个对象对应不同地址
-
-
浅拷贝方法
- Array.concat()、Array.slice()、Array.from()
- Object.assign(newobj,copyobj)拷贝自身可枚举属性
-
深拷贝方法
-
JSON的parse(json字符串反序列化为js对象)与stringify(将js对象序列化为json字符串),但是对于正则表达式或函数类型无法进行深拷贝,还可能会直接丢失相应的值,并且对于函数类型会失去其构造函数,也无法正确处理循环引用,
undefined
、function
、symbol
会在转换过程中被忽略 -
自己写一个深拷贝:利用递归实现每一层都重新创建对象并赋值
-
var deepCopy = function(obj) {
if (typeof obj !== 'object') return;//不是对象就直接结束
var newObj = obj instanceof Array ? [] : {};//判断是数组还是普通对象
for (var key in obj) {//赋值属性或元素
if (obj.hasOwnProperty(key)) {//递归每一层属性
newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key];
}
}
return newObj;
}
- 如何解决循环引用的问题,文章是较为详细的一类:利用hash表,即可检测某对象是否重复出现,重复出现则直接返回||也可以使用数组解决,但是太复杂了
7、service worker
Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用 Service Worker的话,传输协议必须为 HTTPS。因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全。Service Worker 的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。
Service Worker 实现缓存功能一般分为三个步骤:首先需要先注册 Service Worker,然后监听到 install 事件以后就可以缓存需要的文件,那么在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据。
当 Service Worker 没有命中缓存的时候,我们需要去调用 fetch 函数获取数据。也就是说,如果我们没有在 Service Worker 命中缓存的话,会根据缓存查找优先级去查找数据。但是不管我们是从 Memory Cache 中还是从网络请求中获取的数据,浏览器都会显示我们是从 Service Worker 中获取的内容。
8、web worker
在 HTML5 的新规范中,实现了 Web Worker 来引入 JavaScript 的 “多线程” 技术,他的能力让我们可以在页面主运行的 JavaScript 线程中加载运行另外单独的一个或者多个 JavaScript 线程,分为两种类型,专用线程(Dedicated Web Worker) 和共享线程(Shared Web Worker)。专用线程仅能被创建它的脚本所使用(一个专用线程对应一个主线程),而共享线程能够在不同的脚本中使用(一个共享线程对应多个主线程)。
主程序线程和 Worker 线程之间,Worker 线程之间,不会共享任何作用域或资源,他们间唯一的通信方式就是一个基于事件监听机制的 message,Worker 线程和主线程都通过 postMessage()
方法发送消息,通过 onmessage
事件接收消息。在这个过程中数据并不是被共享的,而是被复制的。
在 Worker 线程的运行环境中没有 window 全局对象,也无法访问 DOM 对象,所以一般来说他只能来执行纯 JavaScript 的计算操作
9、解构、扩展,es6的语法,常用API
let/const,块级作用域声明,后者一旦赋值就不会改变,let
声明的变量 a
在其声明之前无法访问,这就是暂时性死区(Temporal Dead Zone)。
- 其实没必要去纠结到底存不存在变量提升,变量提升只是一种语法定义。其实实质就是一段代码在上下文创建阶段(也就是编译阶段)是能够识别到var和let创建的变量的,只会对二者的操作不一样:对var定义的变量初始化为undefined,而let定义的变量仍然处于未初始化状态。也就是为什么报错是‘’x‘’未初始化的原因
解构赋值:将对象的属性或者数组的元素单独提取出来并对应赋值
扩展:展开数组或对象,可用于浅拷贝
箭头函数:简化了函数声明
promise:推出了一种js解决异步编程的方案
Object.assign:浅拷贝,拷贝对象所有可枚举属性
proxy:proxy是在访问目标对象之前的一个拦截器,比之Object.defineProperty更为完备,vue3便使用了proxy来实现数据劫持
10、函数防抖、节流
函数防抖:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。函数防抖就是法师发技能的时候要读条,技能读条没完再按技能就会重新读条。在触发点击事件后,如果用户再次点击了,我们会清空之前的定时器,重新生成一个定时器
函数节流:规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。 函数节流就是fps游戏的射速,就算一直按着鼠标射击,也只会在规定射速内射出子弹。
防抖:搜索框输入后自动搜索、窗口大小 resize 变化后,再重新渲染。
节流:滚动加载更多、高频点击、表单重复提交……
$(document).scroll(deBounce(function () {
console.log(this); //#document
}, 500));
function deBounce(fn, interval) {//防抖
let timer;
return function () {
clearTimeout(timer);//这样对应的定时器就会失效
timer = setTimeout(() =>{//设置新的定时器,从而重新计时
fn.apply(this, arguments);
}, interval);
}
}
let count = 0;
$(document).scroll(throttle(function () {
console.log(`触发了${++count}次!`);
}, 500));
function throttle(fn, wait) {//节流
let timer;
return function () {
if (timer) return;//存在,则说明还没有被执行,要等待自动执行
timer=setTimeout(() => {//设置新的定时器,从而重新开始计时
fn.apply(this, arguments); //让scroll的回调函数的this保持一致
timer=null;//执行一次后就置为null
}, wait);
}
}
11、垃圾回收
应该被分配的内存,没有被分配
-
当不再用到的对象内存,没有及时被回收时,我们叫它
内存泄漏(Memory leak)
。在 JS 中,常见的内存泄露主要有 4 种,全局变量、闭包、DOM 元素的引用、定时器/事件监听器。以及遗忘的map、set,未清理的console(输出引用)。
- 游离的DOM引用:获取一个 DOM 元素的引用,而后面这个元素被删除,由于一直保留了对这个元素的引用,所以它也无法被回收。
-
优化手段:内存优化 ; 手动释放:取消内存的占用即可。
(1)堆内存:fn = null 【null:空指针对象】
(2)栈内存:把上下文中,被外部占用的堆的占用取消即可。
-
排查工具:谷歌浏览器开发者工具
- 打开谷歌的无痕模式(屏蔽插件对内存的影响)
- 找到performance这一栏,勾选memory,然后就能观察内存变化,可以在memory这一栏观察到堆内存的快照,结合手动GC与拍摄快照,可以分析定位内存泄漏
垃圾回收策略
- 标记清除:阶段分为标记与清除,标记所有活动对象,清除非活动对象即没有标记的
- 引用计数:如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收
12、箭头函数和普通函数有什么区别?
(1)箭头函数比普通函数更加简洁
如果没有参数,就直接写一个空括号即可
如果只有一个参数,可以省去参数括号
如果有多个参数,用逗号分割
如果函数体的返回值只有一句,可以省略大括号
如果函数体不需要返回值,且只有一句话,可以给这个语句前面加一个void关键字。最常用的就是调用一个函数:
let fn = () => void doesNotReturn()
(2) 箭头函数没有自己的this
箭头函数不会创建自己的this,所以它没有自己的this,它只会在自己作用域的上一层继承this。所以箭头函数中的this的指向在它在定义时一家确定了,之后不会改变。
(3)箭头函数继承来的this指向永远不会改变,默认绑定外层
(4) call()、apply()、bind()等方法不能改变箭头函数中的this指向
(5) 箭头函数不能作为构造函数使用
(7) 箭头函数没有prototype
(8) 箭头函数不能用作Generator函数,不能使用yeild关键字
13、一道代码输出题
14、转化类数组为数组
- call
- apply
- Array.from(str)
15、js的缺点
- 安全性低,容易受到攻击(虽然有同源策略但仍然无法防止跨站脚本攻击;代码是公开的且在客户端,容易受到恶意攻击与篡改)
- 不同浏览器对js的支持可能不一致,导致页面呈现的效果不一
- JS是弱类型语言,变量类型可在运行时动态改变,可能会导致一些难以调试的错误
- JS是单线程执行
16、数据检测
typeof:能够快速区分基本数据类型,无法将Object、Array、null区分,都返回Object
instanceof:基本数据类型无法判断,实现原理是基于原型链,向上找到原型
Object.prototype.toString.call():每一个继承 Object 的对象都有 toString 方法,如果 toString 方法没有重写的话,会返回 [Object type],其中 type 为对象的类型。但当除了 Object 类型的对象外,其他类型直接使用 toString 方法时,会直接返回都是内容的字符串,所以我们需要使用call或者apply方法来改变toString方法的执行上下文。
原始数据类型与封装对象主要涉及到两个概念:封装和自动装箱。
17、性能优化
性能优化指标:
首屏加载时间First Contentful Paint(FCP):首次内容绘制时间,指浏览器首次绘制页面中至少一个文本、图像、非白色背景色的canvas/svg
元素等的时间,代表页面首屏加载的时间点。
首次绘制时间First Paint(FP):首次绘制时间,指浏览器首次在屏幕上渲染像素的时间,代表页面开始渲染的时间点。
最大内容绘制时间Largest Contentful Paint(LCP):最大内容绘制时间,指页面上最大的可见元素(文本、图像、视频等)绘制完成的时间,代表用户视觉上感知到页面加载完成的时间点。
用户可交互时间Time to Interactive(TTI):可交互时间,指页面加载完成并且用户能够与页面进行交互的时间,代表用户可以开始操作页面的时间点。
页面总阻塞时间Total Blocking Time (TBT):页面上出现阻塞的时间,指在页面变得完全交互之前,用户与页面上的元素交互时出现阻塞的时间。TBT应该尽可能小,通常应该在300毫秒以内。
搜索引擎优化Search Engine Optimization (SEO):网站在搜索引擎中的排名和可见性。评分范围从0到100,100分表示网站符合所有SEO最佳实践。
18、事件冒泡与事件捕获
事件冒泡:事件会从最内层的元素开始发生,一直向上传播,直到碰到document对象。
事件捕获:事件会从最外层开始发生,直到碰到目标元素
这是两个不同模型
事件代理:事件代理(Event Delegation)是一种常见的优化技术,其思想是将*事件处理器* 绑定到一个祖先元素,而不是直接绑定到每个子元素。通过事件冒泡,可以在祖先元素上捕获子元素的事件,从而减少事件处理器的数量,提高性能。addEventListener