2020-05-31
前端, JS, JavaScript
1.关于闭包
什么是闭包?
闭包是有权限访问其它函数作用域内的变量的一个函数。
在js中,变量分为全局变量和局部变量,局部变量的作用域属于函数作用域,在函数执行完以后作用域就会被销毁,内存也会被回收,但是由于闭包是建立在函数内部的子函数,由于其可访问上级作用域的原因,即使上级函数执行完,作用域也不会被销毁,此时的子函数——也就是闭包,便拥有了访问上级作用域中变量的权限,即使上级函数执行完以后作用域内的值也不会被销毁。
闭包解决了什么?
本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。由于闭包可以缓存上级作用域,这样函数外部就可以访问到函数内部的变量。
闭包的应用场景
ajax请求成功的回调
一个事件绑定的回调方法
setTimeout的延时回调
一个函数内部返回另一个匿名函数
闭包的优缺点
优点:让代码更加规范、简介
缺点:使用闭包过多,内存消耗大,造成内存的泄露
2.原型和原型链
这张图看不懂就算了
(1).所有的引用类型都有一个_proto_(隐式原型)属性,属性值是一个普通的对象
(2).所有的函数除了有_proto_属性,还都有一个prototype(显式原型)属性,属性值是一个普通的对象
(3).所有引用类型都有一个constructor(构造函数)属性,该属性(是一个指针)指向它的构造函数
(4).所有引用类型的_proto_属性指向它构造函数的prototype
当一个对象调用自身不存在的属性/方法时,会先去它的_proto_上查找,也就是它的构造函数的prototype;如果没有找到,就会去该构造函数的prototype的proto指向的上一级函数的prototype中查找,最后指向null。这样一层一层向上查找的关系会形成一个链式结构,称为原型链。
深入理解JS的原型和原型链
用自己的方式(图)理解constructor、prototype、proto和原型链
举例理解JS的原型和原型链
3.ES5继承和ES6继承
ES5:组合式继承,先创建子类的实例对象,然后再将父类的方法通过call方法添加到this上,通过原型和构造函数的机制来实现
示例:
1 | // 定义一个父类 |
ES6:先创建父类的实例对象this(所以必须先调用父类的super()方法,然后在用子类的构造函数修改this),通过class关键字定义类,类之间通过extends关键字实现继承,子类必须在constructor方法中调用super方法。因为子类没有自己的this对象,而是继承了父类的this对象,然后对其加工,如果不调用super方法,子类得不到this对象。
示例:
1 | // 定义一个父类 |
4.原生AJAX请求步骤
五步使用法:
(1).创建XMLHTTPRequest对象
(2).使用open方法设置和服务器的交互信息
(3).设置发送的数据,开始和服务器端交互
(4).注册事件
(5).更新界面
Get请求:
1 | // 第一步:创建异步对象 |
POST请求:
1 | // 第一步:创建异步对象 |
5.关于事件委托
什么是事件委托
事件委托也叫事件代理,就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。
事件委托的作用
(1).提高性能:每一个函数都会占用内存空间,只需添加一个时间处理程序代理所有事件,所占用的内存空间更少;
(2).动态监听:使用事件委托可以自动绑定动态添加的元素,即新增的节点不需要主动添加也可以具有和其它元素一样的事件。
实现方式
我们先来看看,如果不用事件委托,需要绑定多个相同事件的时候是如何实现的:
1 |
|
不使用事件委托,那就要遍历每一个li元素,给每个li元素绑定一个点击事件,这样的做法非常耗费内存,如果有100个、1000个li元素,那对性能的影响是非常大的。
那么使用事件委托是怎么实现的呢?
1 |
|
这样一来,通过事件委托,只需要在li元素的父元素ul上绑定一个点击事件,通过事件冒泡的机制,就可以实现li的点击效果。并且通过js动态添加li元素,也能绑定点击事件。
6.null不是一个对象,但为什么typeof null === object
原理是这样的,不同的对象在底层都会表示为二进制,在js中如果二进制的前三位都为0,就会被判断为object类型,null的二进制全为0,自然前三位也是0,所以typeof null === objcet。
7.关于深拷贝和浅拷贝
浅拷贝:只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存
实现:
方法1:直接用=赋值
1 | let obj1 = {a: 1} |
深拷贝:
方法1:用 JSON.stringify 把对象转换成字符串,再用 JSON.parse 把字符串转换成新的对象
1 | let obj1 = { |
方法2:采用递归去拷贝所有层级属性
1 | function deepClone(obj) { |
方法3:lodash函数库实现深拷贝
1 | let obj1 = { |
方法4:通过jQuery的extend方法实现深拷贝
1 | let array = [1,2,3,4] |
方法5:用slice实现对数组的深拷贝
1 | let arr1 = ["1","2","3"] |
方法6:使用扩展运算符实现深拷贝
1 | let obj1 = {brand: "BMW", price: "380000", length: "5米"} |
js浅拷贝与深拷贝的区别和实现方式
8.谈谈js的垃圾回收机制
js拥有自动的垃圾回收机制,当一个值在内存中失去引用时,垃圾回收机制会根据特殊的算法找到它,并将其回收,释放内存。
标记清除法(常用)
(1).标记阶段:垃圾回收器会从根对象开始遍历。每一个可以从根对象访问到的对象都会被添加一个标识,于是这个对象就被标识为可到达对象;
(2).清除阶段:垃圾回收器会对堆内存从头到尾进行线性遍历,如果发现有对象没有被标识为可到达对象,那么就将此对象占用的内存回收,并且将原来标记为可到达对象的标识清除,以便进行下一次垃圾回收操作;
优点:实现简单
缺点:可能会造成大量的内存碎片
引用计数清除法
(1).引用计数的含义就是跟踪记录每个值被引用的次数,当声明了一个变量并将一个引用类型赋值给该变量时,这个值的引用次数就是1。相反,如果包含对这个值引用的变量又取得了另外一个值,这个值的引用次数就减1。
(2).当这个引用次数变成0时,则说明没有办法再访问这个值了,就可以将其所占的内存空间给回收。这样,垃圾收集器下次再运行时,就会释放那些引用次数为0的值所占的内存。
优点:
(1).可即刻回收垃圾
缺点:
(1).计数器值的增减处理繁重
(2).实现繁琐复杂
(3).循环引用无法回收
9.如何阻止事件冒泡和默认事件
标准的DOM对象中可以使用事件对象的stopPropagation()方法来阻止事件冒泡,但在IE8以下中的事件对象通过设置事件对象的cancelBubble属性为true来阻止冒泡
默认事件通过事件对象的preventDefault()方法来阻止,而IE通过设置事件对象的returnValue属性为false来阻止默认事件
10.函数去抖和函数节流
函数去抖(debounce):
当调用函数n秒后,才会执行该动作,若在这n秒内又调用该函数则取消前一次并重新计算执行时间(频繁触发的情况下,只有足够的空闲时间,才执行代码一次)
1 | function debounce(delay, cb) { |
JavaScript专题之跟着underscore学防抖
函数节流(throttle):
函数节流的基本思想是函数预先设定一个执行周期,当调用动作的时刻大于等于执行周期则执行该动作,然后进入下一个新周期(一定时间内js方法只跑一次。比如人的眨眼睛,就是一定时间内眨一次)
1 | function throttle(cb, delay) { |
JavaScript专题之跟着 underscore 学节流
11.谈谈js的事件循环机制
程序开始执行之后,主程序则开始执行同步任务,碰到异步任务就把它放到任务队列之中,等到同步任务全部执行完后,js引擎便去查看任务队列有没有可以执行的异步任务,将异步任务转成同步任务并开始执行,执行完同步任务后继续查看任务队列。这个过程是一直循环的,因此这个过程就是所谓的事件循环,其中任务队列也被称为事件队列。通过一个任务队列,单线程的js实现了异步任务的执行,给人的感觉好像是多线程的。
12.箭头函数和普通函数的区别
普通函数:
(1).this总是代表它的直接调用者
(2).在默认情况下,没找到直接调用者,this指向window
(3).在严格模式下,没有直接调用者的函数中的this是undefined
(4).使用call,apply,bind绑定,this指的是绑定的对象
箭头函数:
(1).在使用 => 定义函数的时候,this的指向是定义时所在的对象,而不是使用时所在的对象,bind()、call()、apply()均无法改变指向
(2).不能用做构造函数,也就是说不能使用new命令,否则就会抛出一个错误
(3).不能使用arguments对象,但是可以使用…rest参数
(4).不能使用yield命令
(5).没有原型属性
13.call()、apply()、bind()的区别
call()、apply()、bind()是用来改变this的指向的
call(): Function.call(obj, param1,param2,param3)
接收到的是param1,param2,param3三个参数
apply(): Function.apply(obj, [param1,param2,param3])
接收到的是param1,param2,param3三个参数
call和apply的区别是参数一个不用[],一个要用[]
bind(): const newFn = Funtion.bind(obj, param1,param2)
返回值是一个函数,需要()来调用
newFn(param3,param4)
接收到的是param1,param2,param3,param4四个参数
14.实现一个sleep函数
js不像java一样有sleep()方法,但由于js是单线程的,可以利用伪死循环阻塞主线程来达到延迟执行的效果
1 | function sleep(delay) { |
15.进程和线程的区别
进程(process):是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位),是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念。
线程(thread):是cpu调度的最小单位(是建立在进程基础上的一次程序运行单位),是进程内可调度的实体,比进程更小的独立运行的基本单位。
一个进程有一个或多个线程,线程之间共同完成进程分配下来的任务,打个比方:
● 假如进程是一个工厂,工厂有它的独立的资源
● 工厂之间相互独立
● 线程是工厂中的工人,多个工人协作完成任务
● 工厂内有一个或多个工人
● 工人之间共享空间
再完善完善概念:
● 工厂的资源 -> 系统分配的内存(独立的一块内存)
● 工厂之间的相互独立 -> 进程之间相互独立
● 多个工人协作完成任务 -> 多个线程在进程中协作完成任务
● 工厂内有一个或多个工人 -> 一个进程由一个或多个线程组成
● 工人之间共享空间 -> 同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)
浅谈浏览器多进程与JS线程
16.ES6、ES7、ES8的新特性
ES6的特性
(1). 类(class)
对熟悉Java、C、C++等语言的开发者来说,class一点都不陌生。ES6引入了class(类),让JS的面向对象编程变得更加简单和易于理解。
(2).模块化(Module)
ES5不支持原生的模块化,在ES6中模块作为重要的组成部分被添加进来。模块的功能主要由export和import组成。每一个模块都有自己单独的作用域,模块之间的相互调用关系是通过 export 来规定模块对外暴露的接口,通过import来引用其它模块提供的接口。同时还为模块创造了命名空间,防止函数的命名冲突。
导出(export)
ES6运行在一个模块中使用export来导出多个变量或函数
1 | // 导出变量 |
导入(import)
定义好模块的输出以后就可以在另外一个模块通过import引用。
1 | import {myModule} from 'myModule' |
(3).箭头(Arrow)函数
这是ES6中最令人激动的特性之一。=>不只是关键字function的简写,它还带来了其它好处。箭头函数与包围它的代码共享同一个this,能很好的解决this的指向问题。
(4).函数参数默认值
ES6支持在定义函数的时候为其设置默认值,当函数的参数为布尔值false时,可以规避一些问题
1 | // 使用默认值 |
(5).模板字符串
1 | // 不使用模板字符串 |
(6).解构赋值
通过解构赋值可以方便的交换两个变量的值:
1 | let a = 1 |
(7).延展操作符(Spread operator)和剩余运算符(rest operator)
当三个点(…)在等号右边,或者放在实参上,是 spread运算符
1 | myFunction(...arr) |
(8).对象属性简写
在ES6中允许我们在设置一个对象的属性的时候不指定属性名
不使用ES6:
1 | const name='Ming',age='18',city='Shanghai'; |
使用ES6:
1 | const name='Ming',age='18',city='Shanghai' |
(9).Promise
Promise 是异步编程的一种解决方案,比传统的解决方案callback更加的优雅。它最早由社区提出和实现的,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。
不使用ES6
1 | setTimeout(function() |
使用ES6
1 | let waitSecond = new Promise(function(resolve, reject) |
(2).指数操作符
在ES7中引入了指数运算符,具有与Math.pow(..)等效的计算结果。
1 | console.log(Math.pow(2, 10)) // 输出1024 |
ES8的特性
(1).async/await
在ES8中加入了对async/await的支持,也就我们所说的异步函数,这是一个很实用的功能。 async/await相当于一个语法糖,解决了回调地狱的问题
(2).Object.values()
Object.values()是一个与Object.keys()类似的新函数,但返回的是Object自身属性的所有值,不包括继承的值。
1 | const obj = { |
(3).Object.entries
Object.entries()函数返回一个给定对象自身可枚举属性的键值对的数组。
1 | const obj = { |
(4).String padding
在ES8中String新增了两个实例函数String.prototype.padStart和String.prototype.padEnd,允许将空字符串或其他字符串添加到原始字符串的开头或结尾
1 | String.padStart(targetLength,[padString]) |
targetLength:当前字符串需要填充到的目标长度。如果这个数值小于当前字符串的长度,则返回当前字符串本身。
padString:(可选)填充字符串。如果字符串太长,使填充后的字符串长度超过了目标长度,则只保留最左侧的部分,其他部分会被截断,此参数的缺省值为 “ “。
1 | console.log('0.0'.padStart(4,'10')) //10.0 |
targetLength:当前字符串需要填充到的目标长度。如果这个数值小于当前字符串的长度,则返回当前字符串本身。
padString:(可选) 填充字符串。如果字符串太长,使填充后的字符串长度超过了目标长度,则只保留最左侧的部分,其他部分会被截断,此参数的缺省值为 “ “;
1 | console.log('0.0'.padEnd(4,'0')) //0.00 |
(5).函数参数列表结尾允许逗号
1 | // 不使用ES8 |
(6).Object.getOwnPropertyDescriptors()
Object.getOwnPropertyDescriptors()函数用来获取一个对象的所有自身属性的描述符,如果没有任何自身属性,则返回空对象。
1 | const obj2 = { |
ES6、ES7、ES8特性一锅炖(ES6、ES7、ES8学习指南)
17.性能监控平台是如何捕获错误的
全局捕获
通过全局的接口,将捕获代码集中写在一个地方,可以利用的接口有:
(1).window.addEventListener(‘error’) / window.addEventListener(“unhandledrejection”) / document.addEventListener(‘click’) 等
(2).框架级别的全局监听,例如aixos中使用interceptor进行拦截,vue、react都有自己的错误采集接口
(3).通过对全局函数进行封装包裹,实现在在调用该函数时自动捕获异常
(4).对实例方法重写(Patch),在原有功能基础上包裹一层,例如对console.error进行重写,在使用方法不变的情况下也可以异常捕获
单点捕获
在业务代码中对单个代码块进行包裹,或在逻辑流程中打点,实现有针对性的异常捕获:
(1).try…catch中throw err
(2).专门写一个函数来收集异常信息,在异常发生时,调用该函数
(3).专门写一个函数来包裹其他函数,得到一个新函数,该新函数运行结果和原函数一模一样,只是在发生异常时可以捕获异常
前端异常监控解决方案研究
18.函数柯里化
在数学和计算机科学中,柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术
示例:
1 | function add(a, b) { |
实现:
(1).最简单的方式,使用lodash库的_.curry
1 | function sum(a, b) { |
(2).自定义函数实现
1 | function curry(func) { |
JavaScript专题之函数柯里化
19.new关键字做了什么?
使用new操作符调用构造函数实际上会经历以下4个步骤:
(1).创建一个新对象
(2).将构造函数的作用域赋给新对象(因此this就指向了这个新对象)
(3).执行构造函数中的代码(为这个新对象添加属性、方法)
(4).返回新对象
1 | var obj = {} |