前言
金九银十,又是一波跑路。趁着有空把前端基础和面试相关的知识点都系统的学习一遍,参考一些权威的书籍和优秀的文章,最后加上自己的一些理解,总结出来这篇文章。适合复习和准备面试的同学,其中的知识点包括:
JavsScript设计模式Vue模块化浏览器HTTP前端安全JavaScript
数据类型
String、Number、Boolean、Null、Undefined、Symbol、BigInt、Object
堆、栈
两者都是存放数据的地方。
栈(stack)是自动分配的内存空间,它存放基本类型的值和引用类型的内存地址。
堆(heap)是动态分配的内存空间,它存放引用类型的值。
JavaScript不允许直接操作堆空间的对象,在操作对象时,实际操作是对象的引用,而存放在栈空间中的内存地址就起到指向的作用,通过内存地址找到堆空间中的对应引用类型的值。
隐式类型转换
JavaScript作为一个弱类型语言,因使用灵活的原因,在一些场景中会对类型进行自动转换。
常见隐式类型转换场景有3种:运算、取反、比较
运算
运算的隐式类型转换会将运算的成员转换为number类型。
基本类型转换:
true+false//1null+10//10false+20//20undefined+30//NaN1+2//12NaN+//NaNundefined+//undefinednull+//null-3//-3null、false、转换number类型都是0undefined转换number类型是NaN,所以undefined和其他基本类型运算都会输出NaN字符串在加法运算(其实是字符串拼接)中很强势,和任何类型相加都会输出字符串(symbol除外),即使是NaN、undefined。其他运算则正常转为number进行运算。引用类型转换:
[1]+10//[]+20//20[1,2]+20//1,[20]-10//10[1,2]-10//NaN({})+10//[objectObject]10({})-10//NaN引用类型运算时,会默认调用toString先转换为string同上结论,除了加法都会输出字符串外,其他情况都是先转string再转number解析引用类型转换过程:
[1,2]+20//过程:[1,2].toString()//1,21,2+20//1,[20]-10//过程[20].toString()//20Number(20)//-10//10取反
取反的隐式类型转换会将运算的成员转换为boolean类型。
这个隐式类型转换比较简单,就是将值转为布尔值再取反:
![]//false!{}//false!false//true通常为了快速获得一个值的布尔值类型,可以取反两次:
!![]//true!!0//false比较
比较分为严格比较===和非严格比较==,由于===会比较类型,不会进行类型转换。这里只讨论==。
比较的隐式类型转换基本会将运算的成员转换为number类型。
undefined==null//true==0//truetrue==1//true1==true//true[1]==1//true[1,2]==1,2//true({})==[objectObject]//trueundefined等于null字符串、布尔值、null比较时,都会转number引用类型在隐式转换时会先转成string比较,如果不相等然再转成number比较预编译
预编译发生在JavaScript代码执行前,对代码进行语法分析和代码生成,初始化的创建并存储变量,为执行代码做好准备。
预编译过程:
创建GO/AO对象(GO是全局对象,AO是活动对象)将形参和变量声明赋值为undefined实参形参相统一函数声明提升(将变量赋值为函数体)例子:
functionfoo(x,y){console.log(x)varx=10console.log(x)functionx(){}console.log(x)}foo(20,30)//1.创建AO对象AO{}//2.寻找形参和变量声明赋值为undefinedAO{x:undefinedy:undefined}//3.实参形参相统一AO{x:20y:30}//4.函数声明提升AO{x:functionx(){}y:30}编译结束后代码开始执行,第一个x从AO中取值,输出是函数x;x被赋值为10,第二个x输出10;函数x已被声明提升,此处不会再赋值x,第三个x输出10。
作用域
作用域能保证对有权访问的所有变量和函数的有序访问,是代码在运行期间查找变量的一种规则。
函数作用域
函数在运行时会创建属于自己的作用域,将内部的变量和函数定义“隐藏”起来,外部作用域无法访问包装函数内部的任何内容。
块级作用域
在ES6之前创建块级作用域,可以使用with或try/catch。而在ES6引入let关键字后,让块级作用域声明变得更简单。let关键字可以将变量绑定到所在的任意作用域中(通常是{...}内部)。
{letnum=10}console.log(num)//ReferenceError:numisnotdefined参数作用域
一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。
letx=1;functionf(x,y=x){console.log(y);}f(2)//2参数y的默认值等于变量x。调用函数f时,参数形成一个单独的作用域。在这个作用域里面,默认值变量x指向第一个参数x,而不是全局变量x,所以输出是2。
letx=1;functionfoo(x,y=function(){x=2;}){x=3;y();console.log(x);}foo()//2x//1y的默认是一个匿名函数,匿名函数内的x指向同一个作用域的第一个参数x。函数foo的内部变量x就指向第一个参数x,与匿名函数内部的x是一致的。y函数执行对参数x重新赋值,最后输出的就是2,而外层的全局变量x依然不受影响。
闭包
闭包的本质就是作用域问题。当函数可以记住并访问所在作用域,且该函数在所处作用域之外被调用时,就会产生闭包。
简单点说,一个函数内引用着所在作用域的变量,并且它被保存到其他作用域执行,引用变量的作用域并没有消失,而是跟着这个函数。当这个函数执行时,就可以通过作用域链查找到变量。
letbarfunctionfoo(){leta=10//函数被保存到了外部bar=function(){//引用着不是当前作用域的变量aconsole.log(a)}}foo()//bar函数不是在本身所处的作用域执行bar()//10优点:私有变量或方法、缓存
缺点:闭包让作用域链得不到释放,会导致内存泄漏
原型链
JavaScript中的对象有一个特殊的内置属性prototype(原型),它是对于其他对象的引用。当查找一个变量时,会优先在本身的对象上查找,如果找不到就会去该对象的prototype上查找,以此类推,最终以Object.prototype为终点。多个prototype连接在一起被称为原型链。
原型继承
原型继承的方法有很多种,这里不会全部提及,只记录两种常用的方法。
圣杯模式
functioninherit(Target,Origin){functionF(){};F.prototype=Origin.prototype;Target.prototype=newF();//还原constuctorTarget.prototype.constuctor=Target;//记录继承自谁Target.prototype.uber=Origin.prototype;}圣杯模式的好处在于,使用中间对象隔离,子级添加属性时,都会加在这个对象里面,不会对父级产生影响。而查找属性是沿着__proto__查找,可以顺利查找到父级的属性,实现继承。
使用:
functionPerson(){this.name=people}Person.prototype.sayName=function(){console.log(this.name)}functionChild(){this.name=child}inherit(Child,Person)Child.prototype.age=18letchild=newChild()ES6Class
classPerson{constructor(){this.name=people}sayName(){console.log(this.name)}}classChildextendsPerson{constructor(){super()this.name=child}}Child.prototype.age=18letchild=newChild()Class可以通过extends关键字实现继承,这比ES5的通过修改原型链实现继承,要清晰和方便很多。
基本包装类型
letstr=hellostr.split()基本类型按道理说是没有属性和方法,但是在实际操作时,我们却能从基本类型调用方法,就像一个字符串能调用split方法。
为了方便操作基本类型值,每当读取一个基本类型值的时候,后台会创建一个对应的基本包装类型的对象,从而让我们能够调用方法来操作这些数据。大概过程如下:
创建String类型的实例在实例上调用指定的方法销毁这个实例letstr=newString(hello)str.split()str=nullthis
this是函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。我理解的this是函数的调用者对象,当在函数内使用this,可以访问到调用者对象上的属性和方法。
this绑定的四种情况:
new绑定。new实例化显示绑定。call、apply、bind手动更改指向隐式绑定。由上下文对象调用,如obj.fn(),this指向obj默认绑定。默认绑定全局对象,在严格模式下会绑定到undefined优先级new绑定最高,最后到默认绑定。
new的过程
创建一个空对象设置原型,将对象的__proto__指向构造函数的prototype构造函数中的this执行对象,并执行构造函数,为空对象添加属性和方法返回实例对象注意点:构造函数内出现return,如果返回基本类型,则提前结束构造过程,返回实例对象;如果返回引用类型,则返回该引用类型。
//返回基本类型functionFoo(){this.name=Joereturnthis.age=20}newFoo()//Foo{name:Joe}//返回引用类型functionFoo(){this.name=Joereturn[]this.age=20}newFoo()//[]call、apply、bind
三者作用都是改变this指向的。
call和apply改变this指向并调用函数,它们两者区别就是传参形式不同,前者的参数是逐个传入,后者传入数组类型的参数列表。
bind改变this并返回一个函数引用,bind多次调用是无效的,它改变的this指向只会以第一次调用为准。
手写call
Function.prototype.mycall=function(){if(typeofthis!==function){throwcallermustbeafunction}letothis=arguments[0]
windowothis._fn=thisletarg=[...arguments].slice(1)letres=othis._fn(...arg)Reflect.deleteProperty(othis,_fn)//删除_fn属性returnres}apply实现同理,修改传参形式即可
手写bind
Function.prototype.mybind=function(oThis){if(typeofthis!=function){throwcallermustbeafunction}letfThis=this//Array.prototype.slice.call将类数组转为数组letarg=Array.prototype.slice.call(arguments,1)letNOP=function(){}letfBound=function(){letarg_=Array.prototype.slice.call(arguments)//new绑定等级高于显式绑定//作为构造函数调用时,保留指向不做修改//使用instanceof判断是否为构造函数调用returnfThis.apply(thisinstanceoffBound?this:oThis,arg.concat(arg_))}//维护原型if(this.prototype){NOP.prototype=this.prototypefBound.prototype=newNOP()}returnfBound}对ES6语法的了解
常用:let、const、扩展运算符、模板字符串、对象解构、箭头函数、默认参数、Promise
数据结构:Set、Map、Symbol
其他:Proxy、Reflect
Set、Map、WeakSet、WeakMap
Set:
成员的值都是唯一的,没有重复的值,类似于数组可以遍历WeakSet:
成员必须为引用类型成员都是弱引用,可以被垃圾回收。成员所指向的外部引用被回收后,该成员也可以被回收不能遍历Map:
键值对的集合,键值可以是任意类型可以遍历WeakMap:
只接受引用类型作为键名键名是弱引用,键值可以是任意值,可以被垃圾回收。键名所指向的外部引用被回收后,对应键名也可以被回收不能遍历箭头函数和普通函数的区别
箭头函数的this指向在编写代码时就已经确定,即箭头函数本身所在的作用域;普通函数在调用时确定this。箭头函数没有arguments箭头函数没有prototype属性Promise
Promise是ES6中新增的异步编程解决方案,避免回调地狱问题。Promise对象是通过状态的改变来实现通过同步的流程来表示异步的操作,只要状态发生改变就会自动触发对应的函数。
Promise对象有三种状态,分别是:
pending:默认状态,只要没有告诉promise任务是成功还是失败就是pending状态fulfilled:只要调用resolve函数,状态就会变为fulfilled,表示操作成功rejected:只要调用rejected函数,状态就会变为rejected,表示操作失败状态一旦改变既不可逆,可以通过函数来监听Promise状态的变化,成功执行then函数的回调,失败执行catch函数的回调
浅拷贝
浅拷贝是值的复制,对于对象是内存地址的复制,目标对象的引用和源对象的引用指向的是同一块内存空间。如果其中一个对象改变,就会影响到另一个对象。
常用浅拷贝的方法:
Array.prototype.sliceletarr=[{a:1},{b:2}]letnewArr=arr1.slice()扩展运算符letnewArr=[...arr1]深拷贝
深拷贝是将一个对象从内存中完整的拷贝一份出来,对象与对象间不会共享内存,而是在堆内存中新开辟一个空间去存储,所以修改新对象不会影响原对象。
常用的深拷贝方法:
JSON.parse(JSON.stringify())JSON.parse(JSON.stringify(obj))手写深拷贝functiondeepClone(obj,map=newWeakMap()){if(obj===null
typeofobj!==object)returnobj;consttype=Object.prototype.toString.call(obj).slice(8,-1)letstrategy={Date:(obj)=newDate(obj),RegExp:(obj)=newRegExp(obj),Array:clone,Object:clone}functionclone(obj){//防止循环引用,导致栈溢出,相同引用的对象直接返回if(map.get(obj))returnmap.get(obj);lettarget=newobj.constructor();map.set(obj,target);for(letkeyinobj){if(obj.hasOwnProperty(key)){target[key]=deepClone(obj[key],map);}}returntarget;}returnstrategy[type]strategy[type](obj)}事件委托
事件委托也叫做事件代理,是一种dom事件优化的手段。事件委托利用事件冒泡的机制,只指定一个事件处理程序,就可以管理某一类型的所有事件。
假设有个列表,其中每个子元素都会有个点击事件。当子元素变多时,事件绑定占用的内存将会成线性增加,这时候就可以使用事件委托来优化这种场景。代理的事件通常会绑定到父元素上,而不必为每个子元素都添加事件。
ul
click=clickHandlerliclass=item1/liliclass=item2/liliclass=item3/li/ulclickHandler(e){//点击获取的子元素lettarget=e.target//输出子元素内容consoel.log(target.textContent)}防抖防抖用于减少函数调用次数,对于频繁的调用,只执行这些调用的最后一次。
/***
param{function}func-执行函数*param{number}wait-等待时间*param{boolean}immediate-是否立即执行*return{function}*/functiondebounce(func,wait=,immediate=false){lettimer,ctx;letlater=(arg)=setTimeout(()={func.apply(ctx,arg)timer=ctx=null},wait)returnfunction(...arg){if(!timer){timer=later(arg)ctx=thisif(immediate){func.apply(ctx,arg)}}else{clearTimeout(timer)timer=later(arg)}}}节流节流用于减少函数请求次数,与防抖不同,节流是在一段时间执行一次。
/***
param{function}func-执行函数*param{number}delay-延迟时间*return{function}*/functionthrottle(func,delay){lettimer=nullreturnfunction(...arg){if(!timer){timer=setTimeout(()={func.apply(this,arg)timer=null},delay)}}}柯里化Currying(柯里化)是把接受多个参数的函数变换成接受一个单一参数的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
通用柯里化函数:
functioncurrying(fn,arr=[]){letlen=fn.lengthreturn(...args)={letconcatArgs=[...arr,...args]if(concatArgs.lengthlen){returncurrying(fn,concatArgs)}else{returnfn.call(this,...concatArgs)}}}使用:
letsum=(a,b,c,d)={console.log(a,b,c,d)}letnewSum=currying(sum)newSum(1)(2)(3)(4)优点:
参数复用,由于参数可以分开传入,我们可以复用传入参数后的函数延迟执行,就跟bind一样可以接收参数并返回函数的引用,而没有调用垃圾回收
堆分为新生代和老生代,分别由副垃圾回收器和主垃圾回收器来负责垃圾回收。
新生代
一般刚使用的对象都会放在新生代,它的空间比较小,只有几十MB,新生代里还会划分出两个空间:form空间和to空间。
对象会先被分配到form空间中,等到垃圾回收阶段,将form空间的存活对象复制到to空间中,对未存活对象进行回收,之后调换两个空间,这种算法称之为“Scanvage”。
新生代的内存回收频率很高、速度也很快,但空间利用率较低,因为让一半的内存空间处于“闲置”状态。
老生代
老生代的空间较大,新生代经过多次回收后还存活的对象会被送到老生代。
老生代使用“标记清除”的方式,从根元素开始遍历,将存活对象进行标记。标记完成后,对未标记的对象进行回收。
经过标记清除之后的内存空间会产生很多不连续的碎片空间,导致一些大对象无法存放进来。所以在回收完成后,会对这些不连续的碎片空间进行整理。
JavaScript设计模式
单例模式
定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
JavaScript作为一门无类的语言,传统的单例模式概念在JavaScript中并不适用。稍微转换下思想:单例模式确保只有一个对象,并提供全局访问。
常见的应用场景就是弹窗组件,使用单例模式封装全局弹窗组件方法:
importVuefromvueimportIndexfrom./index.vueletalertInstance=nullletalertConstructor=Vue.extend(Index)letinit=(options)={alertInstance=newalertConstructor()Object.assign(alertInstance,options)alertInstance.$mount()document.body.appendChild(alertInstance.$el)}letcaller=(options)={//单例判断if(!alertInstance){init(options)}returnalertInstance.show(()=alertInstance=null)}exportdefault{install(vue){vue.prototype.$alert=caller}}无论调用几次,组件也只实例化一次,最终获取的都是同一个实例。
策略模式
定义:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
策略模式是开发中最常用的设计模式,在一些场景下如果存在大量的if/else,且每个分支点的功能独立,这时候就可以考虑使用策略模式来优化。
就像就上面手写深拷贝就用到策略模式来实现:
functiondeepClone(obj,map=newWeakMap()){if(obj===null
typeofobj!==object)returnobj;consttype=Object.prototype.toString.call(obj).slice(8,-1)//策略对象letstrategy={Date:(obj)=newDate(obj),RegExp:(obj)=newRegExp(obj),Array:clone,Object:clone}functionclone(obj){//防止循环引用,导致栈溢出,相同引用的对象直接返回if(map.get(obj))returnmap.get(obj);lettarget=newobj.constructor();map.set(obj,target);for(letkeyinobj){if(obj.hasOwnProperty(key)){target[key]=deepClone(obj[key],map);}}returntarget;}returnstrategy[type]strategy[type](obj)}这样的代码看起来会更简洁,只需要维护一个策略对象,需要新功能就添加一个策略。由于策略项是单独封装的方法,也更易于复用。
代理模式
定义:为一个对象提供一个代用品,以便控制对它的访问。
当不方便直接访问一个对象或者不满足需要的时候,提供一个代理对象来控制对这个对象的访问,实际访问的是代理对象,代理对象对请求做出处理后,再转交给本体对象。
使用缓存代理请求数据:
functiongetList(page){returnthis.$api.getList({page}).then(res={this.list=res.datareturnres})}//代理getListletproxyGetList=(function(){letcache={}returnasyncfunction(page){if(cache[page]){returncache[page]}letres=awaitgetList.call(this,page)returncache[page]=res.data}})()上面的场景是常见的分页需求,同一页的数据只需要去后台获取一次,并将获取到的数据缓存起来,下次再请求同一页时,便可以直接使用之前的数据。
发布订阅模式
定义:它定义对象间的一种一对多的依赖关系,当一个对象的状态发送改变时,所有依赖于它的对象都将得到通知。
发布订阅模式主要优点是解决对象间的解耦,它的应用非常广泛,既可以用在异步编程中,也可以帮助我们完成松耦合的代码编写。像eventBus的通信方式就是发布订阅模式。
letevent={events:[],on(key,fn){if(!this.events[key]){this.events[key]=[]}this.events[key].push(fn)},emit(key,...arg){letfns=this.events[key]if(!fns
fns.length==0){returnfalse}fns.forEach(fn=fn.apply(this,arg))}}上面只是发布订阅模式的简单实现,还可以为其添加off方法来取消监听事件。在Vue中,通常是实例化一个新的Vue实例来做发布订阅中心,解决组件通信。而在小程序中可以手动实现发布订阅模式,用于解决页面通信的问题。
装饰器模式
定义:动态地为某个对象添加一些额外的职责,而不会影响对象本身。
装饰器模式在开发中也是很常用的设计模式,它能够在不影响源代码的情况下,很方便的扩展属性和方法。比如以下应用场景是提交表单。
methods:{submit(){this.$api.submit({data:this.form})},//为提交表单添加验证功能validateForm(){if(this.form.name==){return}this.submit()}}想象一下,如果你刚接手一个项目,而submit的逻辑很复杂,可能还会牵扯到很多地方。冒然的侵入源代码去扩展功能会有风险,这时候装饰器模式就帮上大忙了。
Vue
对MVVM模式的理解
MVVM对应3个组成部分,Model(模型)、View(视图)和ViewModel(视图模型)。
View是用户在屏幕上看到的结构、布局和外观,也称UI。ViewModel是一个绑定器,能和View层和Model层进行通信。Model是数据和逻辑。View不能和Model直接通信,它们只能通过ViewModel通信。Model和ViewModel之间的交互是双向的,ViewModel通过双向数据绑定把View层和Model层连接起来,因此View数据的变化会同步到Model中,而Model数据的变化也会立即反应到View上。
题外话,你可能不知道Vue不完全是MVVM模式:
严格的MVVM要求View不能和Model直接通信,而Vue在组件提供了$refs这个属性,让Model可以直接操作View,违反了这一规定。
Vue的渲染流程
流程主要分为三个部分:
模板编译,parse解析模板生成抽象语法树(AST);optimize标记静态节点,在后续页面更新时会跳过静态节点;generate将AST转成render函数,render函数用于构建VNode。构建VNode(虚拟dom),构建过程使用createElement构建VNode,createElement也是自定义render函数时接受到的第一个参数。VNode转真实dom,patch函数负责将VNode转换成真实dom,核心方法是createElm,递归创建真实dom树,最终渲染到页面上。
data为什么要求是函数
当一个组件被定义,data必须声明为返回一个初始数据对象的函数,因为组件可能被用来创建多个实例。如果data仍然是一个纯粹的对象,则所有的实例将共享引用同一个数据对象!通过提供data函数,每次创建一个新实例后,我们能够调用data函数,从而返回初始数据的一个全新副本数据对象。
JavaScript中的对象作为引用类型,如果是创建多个实例,直接使用对象会导致实例的共享引用。而这里创建多个实例,指的是组件复用的情况。因为在编写组件时,是通过export暴露出去的一个对象,如果组件复用的话,多个实例都是引用这个对象,就会造成共享引用。使用函数返回一个对象,由于是不同引用,自然可以避免这个问题发生。
Vue生命周期
beforeCreate:在实例创建之前调用,由于实例还未创建,所以无法访问实例上的data、