# JavaScript
# 数据类型
- 普通数据类型:string number boolean null undefined symbol BigInt
- 引用数据类型:对象,函数
# 闭包是什么
闭包是指有权访问另一个函数作用域中的变量的函数。
闭包的本质:当前环境中存在指向父级作用域的引用
# 闭包的主要作用
它可以实现变量私有化但同时容易造成内存泄漏,使用场景主要有
- 私有变量
- 自执行函数
- 模拟块级作用域(ES5中没有块级作用域)
- 累加器,迭代器
- 函数赋值,函数当做参数传递
- 函数式编程
- 创建模块
# Promise
# Promise的基本介绍
可以把 Promise 看成一个状态机。初始是 pending
状态,可以通过函数 resolve
和 reject
,将状态转变为resolved
或者rejected
状态,状态一旦改变就不能再次变化
三种状态:
- pending: 初始状态
- resolved: 成功完成
- rejected: 操作失败
Promise 对象的状态改变,只有两种可能:从 Pending 变为 Resolved 和从 Pending 变为 Rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果
# Promise 几个API
- resolve()
- rejected()
- then()
- all()
- race()
- any()
- allSettled()
- catch()
- finally()
Promise.all 可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值
Promise.race 哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态
Promise.any 只要有一个 Promise 实例成功就成功,只有当所有的 Promise 实例失败时 Promise.any 才失败,此时Promise.any 会把所有的失败/错误集合在一起,返回一个失败的 promise 和AggregateError类型的实例
Promise.anySettled 所有异步操作结束了在继续执行下一步操作
anySettled 返回值
[
{ status: 'fulfilled', value: 'p1' },
{ status: 'fulfilled', value: 'p2 延时一秒' },
{ status: 'rejected', value: 'p3 延时两秒' }
]
# Promise 的实现
# async/await对比Promise的优势
- 代码读起来更加同步,Promise虽然摆脱了回调地狱,但是then的链式调⽤也会带来额外的阅读负担
- Promise传递中间值⾮常麻烦,⽽async/await⼏乎是同步的写法,⾮常优雅
- 错误处理友好,async/await可以⽤成熟的try/catch,Promise的错误捕获⾮常冗余
- 调试友好,Promise的调试很差,由于没有代码块,你不能在⼀个返回表达式的箭头函数中设置断点,如果你在⼀个.then代码块中使⽤调试器的步进(step-over)功能,调试器并不会进⼊后续的.then代码块,因为调试器只能跟踪同步代码的每⼀步
# Callback Promise Generator Async 几个API的优劣
# 箭头函数和普通函数的区别
- 箭头函数是匿名函数,不能作为构造函数,不能使用new
- 箭头函数没有原型属性
- 箭头函数的this应该是创建时所在作用域指向的对象
- call、apply、bind方法改变不了箭头函数的指向
- 箭头函数不绑定arguments,取而代之用rest参数解决
const arrow = (...params) => {
console.log(params) //[ 1, 2, 3 ]
console.log(arguments) //[Arguments] {'0': {}, '1': [Function: require] {xxx}, '2': Module {}, xx)
}
function normal(...params) {
console.log(params) //[ 1, 2, 3 ]
console.log(arguments) //{ '0': 1, '1': 2, '2': 3 }
}
const test1 = arrow(1, 2, 3)
const test2 = normal(1, 2, 3)
console.log('arrow.prototype', arrow.prototype) // undefined
console.log('normal.prototype', normal.prototype) //normal {}
# 对深拷贝和浅拷贝的理解
普通数据类型:string number boolean null undefined symbol BigInt
引用数据类型:对象,函数
因为拷贝是对数据进行操作,所以我们得了解一下这两类的数据存储方式; 基本数据类型一般存储在栈中;引用数据类型一般存放在堆中
浅拷贝
- 原理:浅拷贝针对引用类型只会拷贝引用地址,引用地址指向同一个对象,并不会拷贝其中的值
- 结果:当改变原对象或者新对象的值时 会影响另外一个对象的值
深拷贝
- 原理:针对引用类型数据拷贝的是该引用类型的值
- 结果:拷贝对象和被拷贝对象值不会互相影响
区别 深拷贝中既要拷贝基本数据类型也要拷贝引用类型的数据,也就是说拷贝一份完全一样的对象。浅拷贝中之拷贝基本数据类型,引用类型的数据只是拷贝了原来的引用,并没有把引用的数据也拷贝。
# 0.1 + 0.2 === 0.3 嘛?为什么?
在js中,数字是使用Number类型
来表示的(不管是整数还是小数),遵循IEEE 754的规范,使用64位表示一个数字
1 符号位 11 指数位 52 小数位
js在计算的过程中首先要转换为2进制,0.1和0.2在转换的时候会发生无限循环
,然后进行对阶运算,js引擎会对二进制进行截断,所以造成精度丢失
解决方案:
- 三方库: math.js big.js
- 将数字转换为整数 (同时乘以一个倍数,相加后再除以倍数)
- 小于精度
Number.EPSILON
Math.pow(2, -52)
总结:本质上就是计算机在用二进制模拟十进制计算,导致在转换过程中出现的失真问题; 精度丢失可能出现在进制转换和对阶运算中
js的最大值 Math.pow(2, 53) - 1
参考:0.1 + 0.2不等于0.3?为什么JavaScript有这种“骚”操作? (opens new window)
# 一些内存泄露的场景
- 注册事件时未在在组件卸载的时候清除
window.addEventListener('resize', this.handle)
componentWillUnmount - 定时器未取消
- DOM的引用没有清除
- 意外的全局变量:由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收
- 闭包:不合理的使用闭包,从而导致某些变量一直被留在内存当中
参考:一个Vue页面的内存泄露分析 (opens new window)
# ajax axios fetch 的区别
- Ajax 指的是 XMLHttpRequest(XHR),最早出现的发送后端请求技术,隶属于原始js中,核心使用XMLHttpRequest对象,多个请求之间如果有先后关系的话,就会出现回调地狱
- Axios 是一个基于Promise 用于浏览器和 nodejs 的 HTTP 客户端,本质上也是对原生XHR的封装,只不过它是Promise的实现版本,符合最新的ES规范
- fetch号称是AJAX的替代品,是在ES6出现的,使用了ES6中的Promise对象。Fetch是基于promise设计的。Fetch的代码结构比起ajax简单多了,参数有点像jQuery ajax。但是,一定记住fetch不是ajax的进一步封装,而是原生js,没有使用 XMLHttpRequest 对象
Axios的特点:
- 从浏览器中创建 XMLHttpRequest
- 支持 Promise API
- 客户端支持防止CSRF
- 提供了一些并发请求的接口(重要,方便了很多的操作)
- 从 node.js 创建 http 请求
- 拦截请求和响应
- 转换请求和响应数据
- 取消请求
- 自动转换JSON数据
Fetch的特点:
- 符合关注分离,没有将输入、输出和用事件来跟踪的状态混杂在一个对象里
- 更好更方便的写法
fetch是一个低层次的API,你可以把它考虑成原生的XHR,所以使用起来并不是那么舒服,需要进行封装
- fetch只对网络请求报错,对400,500都当做成功的请求,服务器返回 400,500 错误码时并不会 reject,只有网络错误这些导致请求不能完成时,fetch 才会被 reject。
- fetch默认不会带cookie,需要添加配置项:
fetch(url, {credentials: 'include'})
- fetch不支持abort,不支持超时控制,使用setTimeout及Promise.reject的实现的超时控制并不能阻止请求过程继续在后台运行,造成了流量的浪费
- fetch没有办法原生监测请求的进度,而XHR可以
# fetch中如何终端请求
AbortController 接口代表一个控制器对象,允许你在需要时中止一个或多个DOM请求。
- AbortController.signal
- AbortController.abort()
let controller;
const url = "video.mp4";
const downloadBtn = document.querySelector('.download');
const abortBtn = document.querySelector('.abort');
downloadBtn.addEventListener('click', fetchVideo);
abortBtn.addEventListener('click', function() {
if (controller) {
controller.abort();
console.log('Download aborted');
}
});
function fetchVideo() {
controller = new AbortController();
const signal = controller.signal;
fetch(url, { signal })
.then(function(response) {
console.log('Download complete', response);
})
.catch(function(e) {
console.log('Download error: ' + e.message);
});
}
# null和undefined区别
- 都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 undefined 和 null
- undefined 代表的含义是未定义,null 代表的含义是空对象。
- 一般变量声明了但还没有定义的时候会返回 undefined,null主要用于赋值给一些可能会返回对象的变量,作为初始化。
- undefined 在 JavaScript 中不是一个保留字
- 这意味着可以使用 undefined 来作为一个变量名,但是这样的做法是非常危险的,它会影响对 undefined 值的判断。我们可以通过一些方法获得安全的 undefined 值,比如说 void 0。
- 当对这两种类型使用 typeof 进行判断时,Null 类型会返回 “object”,这是一个历史遗留的问题(见下面类型判断部分)。
- 当使用双等号对两种类型的值进行比较时会返回 true,使用三个等号时会返回 false
# defineProperty的属性值有哪些
- value 初始值
- configurable 是否可以删除属性和属性描述
- enumerable 才能出现在对象枚举中
- writable 是否能被赋值运算符改变
- get
- set
let sum = 0;
let obj = {}
Object.defineProperty(obj, 'age', {
// value: '100', //设置初始值,默认为undefined
configurable: false, //是否可删除属性, 默认false 不可删除
enumerable: true, //是否可枚举,默认false 不可枚举
// writable: true, //该属性能否被赋值运算符改变,默认为 false 不可改变
get() {
return sum++
},
set() {
return sum++
}
})
# Proxy的属性值有哪些
proxy 译为 代理,可以拦截属性的一些行为来做一些特殊处理, eg:
const target = {
name: 100
}
const proxy = new Proxy(target, {
get(source, props) {
console.log(source, props);
return source[props]
},
set(source, key, val) {
console.log(source, key, val);
source[key] = val;
}
});
console.log(obj.name); //触发get
obj.name = '12' //触发set
proxy 可以拦截多种行为,eg:
- handler.get:访问属性时触发
- handler.set:属性被赋值时触发
- handler.has:拦截 in 操作,如 'name' in target
- handler.apply:拦截函数调用,如 target(args)
- handler.construct:拦截 new 操作,如 new Target()
- handler.deleteProperty:拦截 delete 操作,如 delete obj.name
- handler.defineProperty:拦截 defineProperty 操作
# defineProperty和proxy的区别
- defineProperty 是 es5 的标准,proxy 是 es6 的标准;
- defineProperty是劫持对象的数据
- 而proxy是整个对象
# 变量提升
什么是变量提升: 所有变量的声明语句都会被提升到代码头部,这就是变量提升。变量提升就是变量在声明之前就可以使用,值为undefined。
变量提升的原理:JS引擎的工作方式是,先解析代码,获取所有被声明的变量,然后再运行,也就是预处理和执行两个阶段。
function test1() {
console.log(1 + a); //ReferenceError: a is not defined
a = 3;
}
function test2() {
console.log(1 + a); // 输出NAN a是undefined
var a = 3;
}
function test3() {
console.log(1 + a); //ReferenceError: Cannot access 'a' before initialization
let a = 3;
}
test1();
test2();
test3();
- let声明的变量,存在块级作用域
- let不允许在同一作用域内重复声明同一个变量
暂时性死区:代码块内,在使用let声明变量之前,该变量都是不可以使用。
# let、const 以及 var 的区别是什么?
- let 和 const 定义的变量
不会出现变量提升
,而 var 定义的变量会提升 - let 和 const 是JS中的块级作用域
- let 和 const 不允许重复声明(会抛出错误)
- let 和 const 定义的变量在定义语句之前,如果使用会抛出错误(形成了暂时性死区),而 var 不会
- const 声明一个只读的常量。一旦声明,常量的值就不能改变(如果声明是一个对象,那么不能改变的是对象的引用地址)
# 获取DOM的位置
可以使用Element.getBoundingClientRect()
node.getBoundingClientRect()
top/left/right/bottom
width
height
# 垃圾回收的原理
垃圾回收的策略
- 标记清除算法
- 引用计数算法
标记清除算法大致过如下:
- 垃圾收集器在运行时会给内存中的所有变量加上一个标记,假设内存中所有对象都是垃圾,全标记为0
- 然后从各个根对象开始遍历,把不是垃圾的节点改成1
- 清除所有标记为0的垃圾,销毁并回收它们所占用的内存空间
- 随后,把所有内存中标记修改为0,等待下一轮垃圾回收
优点
标记清除算法的优点只有一个,就是简单,打标记无非打与不打两种情况,这用二进制位(0和1)就可以为其标记
缺点
标记清除算法,清除垃圾后,剩余的对象内存位置时不变的,会导致空闲的内存空间不连续,出现内存碎片,并且由于剩下空间内存不是一个整块,他是有不同大小的内存组成的内存列表,这就会牵扯出内存分配的问题。
引用计数算法大致过如下:
- 当声明一个变量并将其引用类型赋值给该变量的时候这个值的引用次数就为1
- 如果同一个值又被赋值给另外一个变量,那么引用次数+1
- 如果该变量被赋予了其他值,那么引用次数-1
- 当这个值的引用次数变为0的时候,则说明值没有被使用了,那么这个值没法被访问,垃圾回收器会在执行的时候清理掉引用次数位0的值占用的内存
优点
思路相对标记清除法更清晰。 标记清除法需要每隔一段时间进行一次,那在应用程序(JS脚本)运行过程中线程必须暂停去执行一段时间的GC,另外,标记清除算法需要遍历堆里面的所有对象,来进行后续的操作,而计数只需要在引用时计数就可以了。
缺点 需要一个计数器,而次计数器需要占用很大的位置,因为我们不知道引用数量的上线,最大的缺点是无法解决循环引用无法回收的问题
# Chrome V8中的垃圾回收策略
V8 实现了准确式 GC,GC算法采用了分代式垃圾回收机制
。V8 将内存(堆)分为新生代
和老生代
两部分。
新生代中的对象一般存活时间较短,使用 Scavenge GC
算法。
在新生代空间中,内存空间分为两部分,分别为 From 空间和 To 空间。 在这两个空间中,必定有一个空间是使用的,另一个空间是空闲的。新分配的对象会被放入 From 空间中,当 From 空间被占满时,新生代 GC 就会启动了。算法会检查 From 空间中存活的对象并复制到 To 空间中,如果有失活的对象就会销毁。当复制完成后将 From 空间和 To 空间互换,这样 GC 就结束了
老生代中的对象一般存活时间较长且数量也多,使用了两个算法,分别是标记清除算法和标记压缩算法。
老生代整个流程采用标记清除算法。首先是标记阶段,从一组根元素开始,递归遍历这组根元素,遍历过程中能到达的元素称为活动对象,没有到达的对象可以判断为非活动对象。 清除阶段老生代垃圾回收器会直接将非活动对象内存空间回收,也就是数据清理掉。清除对象后会造成堆内存出现碎片的情况,当碎片超过一定限制后会启动压缩算法。在压缩过程中,将对象向一端移动,直到所有对象都移动完成,然后清理不需要的内存。
# 内存
# 如何判断一个整数 in JavaScript
- option1: 任何数对1取余都为0
- option2: Number.isInteger
- option3: 使用Math.round、Math.ceil、Math.floor 计算完和自己比较
# 如何判断一个变量是不是数组
- 使用 Array.isArray 判断,如果返回 true, 说明是数组
- 使用 instanceof Array 判断,如果返回true, 说明是数组
- 使用
Object.prototype.toString.call
判断,如果值是[object Array]
, 说明是数组 - 通过 constructor 来判断,如果是数组,那么
arr.constructor === Array
(不准确,因为我们可以指定 obj.constructor = Array)
# 类数组和数组
类数组
- 拥有length属性,其它属性(索引)为非负整数(对象中的索引会被当做字符串来处理)
- 不具有数组所具有的方法
类数组可以转换为数组
//第一种方法
Array.prototype.slice.call(arrayLike, start);
//第二种方法
[...arrayLike];
//第三种方法:
Array.from(arrayLike);
# 时间相关
var data = new Date();
console.log(data.getTime())
console.log(Date.now())
console.log(data.toDateString())
console.log(data.toGMTString())
console.log(data.toUTCString())
console.log(data.toISOString())
=> 1536074993162
=> 1536074993162
=> "Tue Sep 04 2018"
=> "Tue, 04 Sep 2018 15:29:53 GMT"
=> "Wed, 05 Sep 2018 03:25:19 GMT"
=> "2018-09-04T15:29:53.162Z"
# CORS怎么让Cookie传过去
要想浏览器处理 CORS 跨域中的 Cookie 只需要分别在网页以及服务端作出一点点改变:网页端中,对于跨域的 XMLHttpRequest 请求,需要设置withCredentials 属性为 true
var xhr = new XMLHttpRequest();
xhr.open("GET", "http://aaa.cn/localserver/api/corsTest");
xhr.withCredentials = true; // 设置跨域 Cookie
xhr.send();
同时服务端的响应中必须携带Access-Control-Allow-Credentials: true
首部。如果服务端的响应中未携带Access-Control-Allow-Credentials: true
首部,浏览器将不会把响应的内容返回给发送者。
要想设置和获取跨域 Cookie,上面提到的两点缺一不可。另外有一点需要注意的是:规范中提到,如果 XMLHttpRequest 请求设置了withCredentials 属性,那么服务器不得设置 Access-Control-Allow-Origin的值为*
,否则浏览器将会抛出The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*'
错误
# 跨域jsonp是同步的?为什么不能传post请求,为什么只能发get请求
JSONP原理:
JSONP的最基本的原理是 - 动态添加一个<script>
标签,而script
标签的src属性是没有跨域的限制的。这样说来,这种跨域方式其实与Ajax XmlHttpRequest
协议无关了
优点:
- 不受同源策略的限制
- 它的兼容性更好
缺点:
- 它只支持GET请求而不支持POST等其它类型的HTTP请求
- 它只支持跨域HTTP请求这种情况,不能解决不同域的两个页面之间如何进行JavaScript调用的问题
# 区分escape、encodeURI和encodeURIComponent
escape是对字符串(string)进行编码(而另外两种是对URL)
- encodeURI 不会对下列字符编码 ASCII字母、数字、~!@#$&*()=:/,;?+'
- encodeURIComponent 不会对下列字符编码 ASCII字母、数字、~!*()'_
# ES5和ES6的区别
ES6的新特性:
- 块级作用域 关键字let, 常量const
- 对象字面量的属性赋值简写
- 赋值解构
- 默认参数
- 箭头函数
- 字符串模板
- Generators + Iterators
- Class
- Modules
- Map + Set + WeakMap + WeakSet
- Symbols
- Promise
- 一些API:isArray / from / of 方法;数组实例新增了 entries(),keys() 和 values() 等方法
# JS作用域有哪些
在ES5中,js只有两种形式的作用域:全局作用域
和函数作用域
,在ES6中新增了块级作用域
# 变量声明前置与函数声明前置
变量声明会在代码执行之前就创建、初始化并赋值undefined,但是变量声明会提升,变量的赋值不会提升!!!
函数声明提升:执行代码之前会先读取函数的声明。这意味着可以把函数的声明放在函数的调用的后面
Note:变量声明和函数声明都会提升,函数声明提升的优先级高于变量声明提升
# new创建函数时会发生哪些过程
- 创建一个全新的对象
- 这个新对象会执行[[Prototype]]连接
- 这个新对象会绑定到函数调用的this
- 如果函数没有返回其他对象,会返回这个新对象
function new(Con, ...args) {
// 创建一个空的对象
let obj = {};
// 链接到原型
Object.setPrototypeOf(obj, Con.prototype); // obj.__proto__ = Con.prototype;
// 绑定 this,执行构造函数
let result = Con.apply(obj, ...args);
// 判断构造函数返回值是否为对象,如果为对象就使用构造函数返回的值,否则使用obj
return typeof result === 'object' ? result : obj
}
new 一个构造函数,如果函数返回 return {} 、 return null , return 1 , return true 会发生什么情况?
如果函数返回一个对象,那么new 这个函数调用返回这个函数的返回对象,否则返回 new 创建的新对象
# DOMContentLoaded 与onload区别
- 1.当 onload 事件触发时,页面上所有的DOM,样式表,脚本,图片,flash都已经加载完成了
- 2.当 DOMContentLoaded 事件触发时,仅当DOM加载完成,不包括样式表,图片,flash
# 如何处理函数的argument参数
分析:首先argument参数并不是一个Array
,所以不能使用数组的方法去处理。想要处理,首先需要将其转化为真实的Array
Array.prototype.slice.call(arguments);
[].slice.call(arguments);
or
Array.from(...arguments)
or
[...arguments]
or
function (...args){}
# 如何去创建一个新的数组(浅拷贝一个新的数组)
const array = [1,2,3,4,5]
1.contact
const copyArray = [].concat(array);
2.slice
const copyArray = copyArray.slice();
//or
const copyArray = [].slice.call(array)
# Array 相关的操作API
- Array.prototype.splice() //添加,删除,拼接
- Array.prototype.slice() //切割数组
splice() 删除元素,并向数组添加新元素。直接对原数组进行修改
const test1 = [1, 2, 3, 4, 5];
console.log(test1.splice(0));
console.log(test1);
[1, 2, 3, 4, 5]
[]
const test2 = [1, 2, 3, 4, 5];
console.log(test2.splice(0, 2)); //<=== 不包括2
console.log(test2);
[1,2]
[3,4,5]
const test3 = [1, 2, 3, 4, 5];
console.log(test3.splice(1, 2, 8));
console.log(test3);
[2,3]
[1,8,4,5]
slice() 从某个已有的数组返回选定的元素; 返回一个子数组,对原数组无影响
const test1 = [1, 2, 3, 4, 5];
console.log(test1.slice(0));
[1,2,3,4,5]
// 原数组不变
const test2 = [1, 2, 3, 4, 5];
console.log(test2.slice(0, 2)); //<=== 不包括2
[1,2]
// 原数组不变
# String相关的API
- String.prototype.slice() //切割字符串
- String.prototype.splice() //string 无此方法
# 正则表达式
以^开头
以$结尾
* 表达式不出现或出现任意次
+ 表达式至少出现1次
?匹配表达式0次或者1次
return /[aeiou]$/i.test(str);
正则表达式的方法
RegExp.compile() //即将弃用
RegExp.exec()
RegExp.test()
String.match(regExp)
# 为什么子类的原型要指向父类的实例而不是父类的原型?
eg:
Student.prototype = People.prototype;
Student.prototype.constructor = Student;
eg:
Student.prototype = new People();
Student.prototype.constructor = Student;
分析:
如果直接写Student.prototype = People.prototype
,那你对Student的prototype的任何修改都会同时修改People的prototype。也就意味着Student
和People
是一样的了。
# call apply bind的区别
共性: 都是为了解决改变 this 的指向
- call 该方法返回函数运行结果,除了第一个参数,接受的是一个
参数列表
fn.call(obj, arg1, arg2, ...)
- apply 该方法返回函数运行结果,除了第一个参数,接受的是一个
参数数组
fn.apply(obj, [argsArray])
- bind 该方法会返回一个函数
示例
getValue.call(a, 'yck', '24')
getValue.apply(a, ['yck', '24'])
getValue = getValue.bind(a);
# 如何判断对象类型
- 可以通过
Object.prototype.toString.call(xx)
。这样我们就可以获得类似 [object Type] 的字符串。 - instanceof 可以正确的判断对象的类型,因为内部机制是通过判断对象的原型链中是不是能找到类型的 prototype
# 防抖、截流
防抖核心:延迟执行
实际运用
- 1.对于按钮防点击来说的实现:一旦我开始一个定时器,只要我定时器还在,不管你怎么点击都不会执行回调函数。一旦定时器结束并设置为 null,就可以再次点击了。
- 2.就是用于input输入框架的格式验证
- 3.提交按钮的点击事件
- 4.实时搜索
- 6.给按钮加函数防抖防止表单多次提交
- 7.对于输入框连续输入进行AJAX验证时,用函数防抖能有效减少请求次数
- 8.判断scroll是否滑到底部,滚动事件+函数防抖
节流核心:让一个函数不要执行得太频繁,减少一些过快的调用来节流。
实际运用
- 1.(页面滚动) 比如在页面的无限加载场景下,我们需要用户在滚动页面时,每隔一段时间发一次 Ajax 请求,而不是在用户停下滚动页面操作时才去请求数据。这样的场景,就适合用节流技术来实现。
- 2.窗口调整
- 3.抢购疯狂点击
- 4.游戏中的刷新率
- 5.Canvas画笔功能
总的来说,适合多次事件一次响应的情况
# 数组去重
方法一:
function unique(array) {
var obj = {};
return array.filter(function(item, index, array){
return obj.hasOwnProperty(item) ? false : (obj[item] = true)
})
}
function unique(array) {
return Array.from(new Set(array));
}
Array.from(new Set([1, 1, 2, 2]))
# 高度,宽度,位置
- offsetWidth border + padding + content 没有margin
- offsetHeight border + padding + content 没有margin
- offsetLeft offsetTop 一般是相对于offsetParent计算的
- clientWidth padding + content 没有border
- clientHeight padding + content 没有border
- event.clientX 是目标点距离浏览器可视范围的X轴坐标
- event.clientY 是目标点距离浏览器可视范围的Y轴坐标
- event.pageX 是目标点距离document最左上角的X轴坐标
- event.pageY 是目标点距离document最左上角的Y轴坐标
# 介绍下 Set、Map、WeakSet 和 WeakMap 的区别?
Set 集合 类似数组 Set对象允许你存储任何类型的值,无论是原始值或者是对象引用。它类似于数组,但是成员的值都是唯一的,没有重复的有add,delete,has,clear方法 可以用来做数组去重
WeakSet
WeakSet中所有的值是唯一的,且都是对象,不可遍历
,持有的都是弱引用。有add,delete,has
Map 字典 类似对象 Map 对象保存键值对,任何值(对象或者原始值) 都可以作为一个键或一个值 键值对的形式,可以遍历,有set,get, delete,has,size,clear方法
WeakMap
只接受对象作为键名
,不接受其他类型的值作为键名
键名所指向的对象,不计入垃圾回收机制
不能遍历,有set,delete,has方法
WeakMap的设计目的在于,有时想在某个对象上面存放一些数据,但是这会形成对于这个对象的引用。一旦不再需要这两个对象,就必须手动删除这个引用,否则垃圾回收机制就不会释放对象占用的内存。 而WeakMap的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用
# 原型/原型链的关系
原型:在 JavaScript 中,每当定义一个对象(函数也是对象)时候,对象中都会包含一些预定义的属性。其中每个函数对象都有一个prototype 属性,这个属性指向函数的原型对象。使用原型对象的好处是所有对象实例共享它所包含的属性和方法。
原型链:原型链解决的主要是继承问题
每个对象拥有一个原型对象,通过 __proto__
指针指向其原型对象,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层一层,最终指向 null(Object.prototype.__proto__
指向的是null)。这种关系被称为原型链 (prototype chain),通过原型链一个对象可以拥有定义在其他对象中的属性和方法。
# typeof 是否正确判断类型? instanceof呢?instanceof 的实现原理是什么?
typeof 能够正确的判断基本数据类型,但是除了 null。 特例:typeof null输出的是对象
typeof {} //object
typeof Date //function
instanceof 可以准确的判断复杂数据类型,但是不能正确判断基本数据类型
let date = new Date()
date instanceof Date;
instanceof 是通过原型链判断的,A instanceof B
在A的原型链中层层查找是否有原型等于B.prototype
如果一直找到A的原型链的顶端(null 即Object.prototype.__proto__
),仍然不等于B.prototype,那么返回false,否则返回true;
function _instanceof(instance, fn) {
let prototype = instance.__proto__;
while (prototype !== null) {
if (prototype === fn.prototype) {
return true;
} else {
prototype = prototype.__proto__;
}
}
return false;
}
# ES6中的class和ES5的类有什么区别?
- ES6 class 内部所有定义的方法都是不可枚举的
- ES6 class 必须使用 new 调用
- ES6 class 不存在变量提升
- ES6 class 默认即是严格模式
- ES6 class 子类必须在构造函数中调用super(),这样才有this对象; ES5中类继承的关系是相反的,先有子类的this,然后用父类的方法应用在this上
# 如何正确的判断this,箭头函数的this
this的绑定规则有四种:默认绑定,隐式绑定,显式绑定,new绑定
- 默认绑定:如果在严格模式下,则绑定到 undefined,否则绑定到全局对象
- 隐式绑定:函数是否在某个上下文对象中调用(隐式绑定),如果是的话,this 绑定的是那个上下文对象
- 显示绑定:通过call,apply,bind等方法就行绑定
- new绑定:函数是否在 new 中调用(new绑定),如果是,那么 this 绑定的是new中新创建的对象
箭头函数没有自己的 this, 它的this继承于上一层代码块的this
# JavaScript执行上下文栈和作用域链的理解
执行上下文就是当前 JavaScript 代码被解析和执行时所在环境, JS执行上下文栈可以认为是一个存储函数调用的栈结构,遵循先进后出的原则
- JavaScript执行在单线程上,所有的代码都是排队执行。
- 一开始浏览器执行全局的代码时,首先创建全局的执行上下文,压入执行栈的顶部。
- 每当进入一个函数的执行就会创建函数的执行上下文,并且把它压入执行栈的顶部。当前函数执行-完成后,当前函数的执行上下文出栈,并等待垃圾回收。
- 浏览器的JS执行引擎总是访问栈顶的执行上下文。
- 全局上下文只有唯一的一个,它在浏览器关闭时出栈。
作用域: ES5 中只存在两种作用域:全局作用域和函数作用域。ES6新增了块级作用域. 在 JavaScript 中,我们将作用域定义为一套规则,这套规则用来管理js引擎如何在当前作用域以及嵌套子作用域中根据标识符名称进行变量(变量名或者函数名)查找。
规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。换句话说,作用域决定了代码区块中变量和其他资源的可见性。(全局作用域、函数作用域、块级作用域)
作用域链: 无论是 LHS 还是 RHS 查询,都会在当前的作用域开始查找,如果没有找到,就会向上级作用域继续查找目标标识符,每次上升一个作用域,一直到全局作用域为止。
从当前作用域开始一层层往上找某个变量,如果找到全局作用域还没找到,就放弃寻找 。这种层级关系就是作用域链。(由多个执行上下文的变量对象构成的链表就叫做作用域链,学习下面的内容之后再考虑这句话)
需要注意的是,js 采用的是静态作用域,所以函数的作用域在函数定义时就确定了
# 继承
原型链继承
function SuperType() {
this.superName = 'super';
}
SuperType.prototype.getSuperName = function() {
return this.superName;
};
function SubType() {
this.subName = 'sub';
}
SubType.prototype = new SuperType(); //基本满足需求,但是会产生一些副作用,例如在SuperType的构造函数中有一些额外的操作,就会影响到SubType本身
SubType.prototype.getSubName = function() {
return this.subName;
};
var ins = new SubType();
console.log(ins.getSuperName()); //'super'
实现原型链的本质就是重写原型对象,替换为一个新类型的实例
组合继承
function SuperType(name) {
this.name = name;
this.colors = ['red', 'blue']
}
SuperType.prototype.sayName = function() {
console.log(this.name)
};
function SubType(name, age) {
//继承属性 //第二次调用
SuperType.call(this, name);
this.age = age;
}
//继承方法
SubType.prototype = new SuperType(); //第一次调用
SubType.prototype.constructor = SubType; //如果不添加,原型(父类的实例)上是没有这个属性的
SubType.prototype.sayAge = function() {
console.log(this.age)
};
var ins3 = new SubType('yixing', 24);
ins3.colors.push('green');
console.log(ins3.colors); //'red,blue,green'
ins3.sayName(); //'yixing'
ins3.sayAge(); //24
var ins4 = new SubType('xiaoya', 23);
ins4.colors.push('yellow');
ins4.sayName(); //'xiaoya'
ins4.sayAge(); //23
组合继承的缺陷
无论什么情况下,都会调用两次父类型的构造函数:一次是在创建子类型的原型时,另一次是在子类型的构造函数内部。 这会导致在第一次调用时,会将父类的所有的实例属性挂载到子类的原型上;第二次调用在新对象上创建了自己的新的实例属性,实现了覆盖的效果
寄生组合继承
function SuperType(name) {
this.name = name;
this.colors = ['red', 'blue']
}
SuperType.prototype.sayName = function() {
console.log(this.name)
};
function SubType(name, age) {
//继承属性 //第二次调用
SuperType.call(this, name);
this.age = age;
}
//继承方法
SubType.prototype = Object.create(SuperType.prototype); //第一次调用
SubType.prototype.constructor = SubType; //如果不添加,原型(父类的实例)上是没有这个属性的
SubType.prototype.sayAge = function() {
console.log(this.age)
};
var ins3 = new SubType('yixing', 24);
ins3.colors.push('green');
console.log(ins3.colors); //'red,blue,green'
ins3.sayName(); //'yixing'
ins3.sayAge(); //24
var ins4 = new SubType('xiaoya', 23);
ins4.colors.push('yellow');
ins4.sayName(); //'xiaoya'
ins4.sayAge(); //23
# 取数组的最大值
// ES5 的写法
Math.max.apply(null, [14, 3, 77, 30]);
// ES6 的写法
Math.max(...[14, 3, 77, 30]);
// reduce
[14,3,77,30].reduce((accumulator, currentValue)=>{
return accumulator = accumulator > currentValue ? accumulator : currentValue
});
# 函数柯里化
函数柯里化是指 把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数(也称作部分求值函数)
- 参数复用
- 提前确认
- 延迟运行
# 事件流
事件流是网页元素接收事件的顺序,"DOM2级事件"规定的事件流包括三个阶段:
- 事件捕获阶段
- 处于目标阶段
- 事件冒泡阶段
首先发生的事件捕获,为截获事件提供机会。然后是实际的目标接受事件。最后一个阶段是事件冒泡阶段,可以在这个阶段对事件做出响应。 虽然捕获阶段在规范中规定不允许响应事件,但是实际上还是会执行,所以有两次机会获取到目标对象
# 事件是如何实现的
基于发布订阅模式,就是在浏览器加载的时候会读取事件相关的代码,但是只有实际等到具体的事件触发的时候才会执行
- DOM0 级事件,直接在 html 元素上绑定 on-event,比如 onclick,取消的话,dom.onclick = null,同一个事件只能有一个处理程序,后面的会覆盖前面的。
- DOM2 级事件,通过 addEventListener 注册事件,通过 removeEventListener 来删除事件,一个事件可以有多个事件处理程序,按顺序执行,捕获事件和冒泡事件
- DOM3级事件,增加了事件类型,比如 UI 事件,焦点事件,鼠标事件
# symbol 有什么用处
- symbol是唯一的,所以可以用来表示一个独一无二的变量防止命名冲突。
- 还可以利用 symbol 不会被常规的方法Object.keys()或者for...in遍历到(除了
Object.getOwnPropertySymbols
),所以可以用来模拟私有变量。
主要用来提供遍历接口,实现了 Symbol.iterator
的对象才可以使用 for···of
循环,可以统一处理数据结构。调用之后回返回一个遍历器对象,包含有一个 next 方法,使用 next 方法后有两个返回值 value 和 done 分别表示函数当前执行位置的值和是否遍历完毕
const obj_iterator = {
name: 'this is a object without iterator',
test: '100',
[Symbol.iterator]() {
let index = 0;
return {
next: () => {
let keys = Object.keys(this);
return index < keys.length ? {
done: false,
value: this[keys[index++]]
} : {
done: true
}
}
}
}
};
for (let v of obj_iterator) {
console.log(v)
}
# PWA
渐进式网络应用(PWA)是谷歌在2015年底提出的概念。基本上算是web应用程序,但在外观和感觉上与原生app类似。
支持PWA的网站可以提供脱机工作
、推送通知
和设备硬件访问
等功能
# ServiceWorker
Service Worker是浏览器在后台独立于网页运行的脚本,它打开了通向不需要网页或用户交互的功能的大门。
现在,它们已包括如推送通知
和后台同步
等功能。
将来,Service Worker将会支持如定期同步或地理围栏等其他功能。
# 如果一个构造函数,bind了一个对象,用这个构造函数创建出的实例会继承这个对象的属性吗
不会继承,因为根据 this 绑定四大规则,new 绑定的优先级高于 bind 显示绑定,通过 new 进行构造函数调用时,会创建一个新对象,这个新对象会代替 bind 的对象绑定,作为此函数的 this,并且在此函数没有返回对象的情况下,返回这个新建的对象。
# 重绘与重排
重排/回流(Reflow):当DOM的变化影响了元素的几何信息,浏览器需要重新计算元素的几何属性,将其安放在界面中的正确位置,这个过程叫做重排。表现为重新生成布局,重新排列元素
重绘(Repaint): 当一个元素的外观发生改变,但没有改变布局,重新把元素外观绘制出来的过程,叫做重绘。表现为某些元素的外观被改变
# 如何触发重排/重绘
重排:页面的首次渲染,浏览器的窗口大小发生变化,插入删除元素、元素尺寸改变(边距、填充、边框、宽度和高度)
重绘:更改元素的样式 颜色,背景色,阴影
# 如何避免重绘或者重排
- 集中改变样式,不要一条一条地修改 DOM 的样式
- 不要把 DOM 结点的属性值放在循环里当成循环里的变量
- 为动画的 HTML 元件使用 fixed 或 absolute 的 position,那么修改他们的 CSS 是不会 reflow 的
- 动画开始GPU加速,translate使用3D变化
- 提升为合成层
- 避免触发布局操作和事件
transform 不重绘,不回流
是因为transform属于合成属性,对合成属性进行transition/animate
动画时,将会创建一个合成层。这使得动画元素在一个独立的层中进行渲染。当元素的内容没有发生改变,就没有必要进行重绘。浏览器会通过重新复合来创建动画帧。
将元素提升为合成层有以下优点:
- 合成层的位图,会交由 GPU 合成,比 CPU 处理要快
- 当需要 repaint 时,只需要 repaint 本身,不会影响到其他的层
- 对于 transform 和 opacity 效果,不会触发 layout 和 paint
# 硬件加速GPU
CSS3硬件加速又叫做GPU加速,是利用GPU进行渲染,减少CPU操作的一种优化方案。由于GPU中的transform等CSS属性不会触发回流重绘,所以能大大提高网页的性能。 CSS中的以下几个属性能触发硬件加速:
- transform
- opacity
- filter
- will-change
如果有一些元素不需要用到上述属性,但是需要触发硬件加速效果,可以使用一些小技巧来诱导浏览器开启硬件加速
# html5语义化
语义化标签:
- header
- nav
- main
- article
- section
- aside
- footer
语义化的优点有:
- 代码结构清晰,易于阅读,利于开发和维护
- 方便其他设备解析(如屏幕阅读器)根据语义渲染网页。
- 有利于搜索引擎优化(SEO),搜索引擎爬虫会根据不同的标签来赋予不同的权重
# Web安全 XSS
XSS(Cross-Site Scripting,跨站脚本攻击
)是一种代码注入攻击
。攻击者在目标网站上注入恶意代码,当被攻击者登陆网站时就会执行这些恶意代码,这些脚本可以读取 cookie,session tokens,或者其它敏感的网站信息,对用户进行钓鱼欺诈,甚至发起蠕虫攻击等
XSS 可以分为存储型
、反射型
和 DOM 型
危害:
- 构造Get,Post请求,获取敏感信息
- XSS钓鱼
- 识别用户浏览器
- 识别用户安装的软件
- 获取用户的真实IP
- 流量窃取
- 钓鱼欺诈
如何防御:
- url参数使用encodeURIComponent方法转义
- 尽量不是有InnerHtml插入HTML内容
- 使用特殊符号、标签转义符,对于可能存在的问题前端加以判断
- 使用CSP(Content-Security-Policy),CSP 的本质是建立一个白名单,告诉浏览器哪些外部资源可以加载和执行,从而防止恶意代码的注入攻击
- 对一些敏感信息进行保护,比如 cookie 使用 http-only,使得脚本无法获取。也可以使用验证码,避免脚本伪装成用户执行一些操作
# Web安全 CSRF
CSRF(Cross-site request forgery)跨站请求伪造:攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。
如何防御:
- 验证HTTP Referer字段(不够安全)
- 添加验证码
- 使用随机token
- 服务端给用户生成一个token,加密后传递给用户
- 用户在提交请求时,需要携带这个token
- 服务端验证token是否正确
- 在设置 cookie 属性的时候设置
sameSite
,限制 cookie 不能作为被第三方使用
# Web安全 DDOS
DDoS又叫分布式拒绝服务,全称 Distributed Denial of Service,其原理就是利用大量的请求造成资源过载,导致服务不可用。
如何防御:
- 限制单IP请求频率
- 防火墙等防护设置禁止ICMP包等
- 检查特权端口的开放
# JavaScript如何做隐式类型转换的
JavaScript 中每个值隐含的自带ToPrimitive
方法,用来将值 (无论是基本类型值还是对象)转换为基本类型值。如果值为基本类型,则直接返回值本身;如果值为对象,则需要遵守规则:
type的值为number或者string
1)当type为number时规则如下:
- 调用obj的valueOf方法,如果为原始值,则返回,否则下一步;
- 调用obj的toString方法,后续同上;
- 抛出TypeError 异常。
2)当type为string时规则如下:
- 调用obj的toString方法,如果为原始值,则返回,否则下一步;
- 调用obj的valueOf方法,后续同上;
- 抛出TypeError 异常。
# 严格模式 use strict
use strict 是一种 ECMAscript5 添加的(严格模式)运行模式,这种模式使得 Javascript 在更严格的条件下运行。设立严格模式的目的如下:
- 消除 Javascript 语法的不合理、不严谨之处,减少怪异行为;
- 消除代码运行的不安全之处,保证代码运行的安全;
- 提高编译器效率,增加运行速度;
- 为未来新版本的 Javascript 做好铺垫。
区别:
禁止使用 with 语句。 禁止 this 关键字指向全局对象。 对象不能有重名的属性
# async 和 defer 的区别
如果没有defer或async属性,浏览器会立即加载并执行相应的脚本。它不会等待后续加载的文档元素,读取到就会开始加载和执行,这样就阻塞了后续文档的加载
defer 和 async属性都是去异步加载外部的JS脚本文件,它们都不会阻塞页面的解析,其区别如下:
- 执行顺序: 多个带async属性的标签,不能保证加载的顺序;多个带defer属性的标签,按照加载顺序执行;
- 脚本是否并行执行:async属性,表示后续文档的加载和执行与js脚本的加载和执行是并行进行的,即异步执行;defer属性,加载后续文档的过程和js脚本的加载(此时仅加载不执行)是并行进行的(异步),js脚本需要等到文档所有元素解析完成之后才执行,
DOMContentLoaded
事件触发执行之前