1、浅拷贝与深拷贝
【】Object.assign()
与 扩展运算符可以实现浅拷贝,也可通过循环赋值实现。数组还可以使用 slice() / concat()
方法实现。浅拷贝的局限性在于它只能解决第一层的问题,如果子属性也是对象就无能为力
// Object.assign() 方法将所有可枚举(Object.propertyIsEnumerable() 返回 true)的自有(Object.hasOwnProperty() 返回 true)
// 属性从一个或多个源对象复制到目标对象,返回修改后的对象
let obj = {
age:'24'
}
let objCopy = Object.assign({},obj);
objCopy.age=15
console.log(obj.age); // 24
//扩展运算符方法
let obj = {
age:'24'
}
let objCopy = {...obj};
objCopy.age=15
console.log(obj.age); // 24
【】深拷贝可以使用JSON.parse(JSON.stringify(obj))
实现,但是它会忽略 undefind 、Symbol 、不能序列化函数、不能解决循环引用的对象。下面是完美深拷贝:
/**
* 深拷贝
* @param {Object} obj 要拷贝的对象
* @param {Map} map 用于存储循环引用对象的地址
*/
function deepClone(obj = {}, map = new Map()) {
if (typeof obj !== "object") {
return obj;
}
if (map.get(obj)) {
return map.get(obj);
}
let result = {};
// 初始化返回结果
if (
obj instanceof Array ||
// 加 || 的原因是为了防止 Array 的 prototype 被重写,Array.isArray 也是如此
Object.prototype.toString(obj) === "[object Array]"
) {
result = [];
}
// 防止循环引用
map.set(obj, result);
for (const key in obj) {
// 保证 key 不是原型属性
if (obj.hasOwnProperty(key)) {
// 递归调用
result[key] = deepClone(obj[key], map);
}
}
// 返回结果
return result;
}
2、垃圾回收机制、内存泄漏、闭包
【】现在常用的垃圾回收机制是标记清楚法
,即垃圾收集器会从根对象(Window对象)出发,扫描所有可以触及的对象,不可触及的对象会被垃圾回收
- 将对象分为
新生代
和老生代
,新生代又被分为From
和To
两个空间,当 From 空间满后会执行 Scavenge 算法进行垃圾回收:首先检查 From 空间的存活对象,如果满足晋升老生代条件,则晋升到老生代,如果不满足条件则移动 To 空间;如果对象不存货则释放对象空间;最后将 From 空间和 To 空间进行交换 - 新生代晋升老生代有两个条件:一是对像是否已经经过一次 Scavenga 回收;二是当对象从 From 复制到 To 时,To 的空间使用超过 25%
- 最后对老生代采用标记清除法
【】该释放的内存垃圾没有被释放,依然霸占着原有的内存不松手,造成系统内存的浪费,导致性能恶化,系统崩溃等严重后果,这就是所谓的内存泄漏
【】在某个内部函数的执行上下文创建时,会将父级函数的活动对象加到内部函数的 [[scope]] 中,形成作用域链,所以即使父级函数的执行上下文销毁(即执行上下文栈弹出父级函数的执行上下文),但是因为其活动对象还是实际存储在内存中可被内部函数访问到的,从而实现了闭包
。闭包的变量会常驻内存,滥用闭包容易造成内存泄漏
3、闭包应用——柯里化、偏函数
【】柯里化 就是将 n 个参数的 1 个函数改为只接受一个参数的 n 个互相嵌套的函数
//原函数
function getAddress(province,city,area){
return province + city + area;
}
getAddress('浙江省','杭州市','西湖区'); //浙江省杭州市西湖区
//柯里化后
function getAddress(province){
return function (city) {
return function (area) {
return province + city + area;
}
}
}
getAddress('浙江省')('杭州市')('西湖区'); //浙江省杭州市西湖区
【】函数柯里化实现
// 函数柯里化指的是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。
function curry(fn, args) {
let length = fn.length;
args = args || [];
return function() {
let subArgs = args.slice(0);
// 拼接得到现有的所有参数
for (let i = 0; i < arguments.length; i++) {
subArgs.push(arguments[i]);
}
// 判断参数的长度是否已经满足函数所需参数的长度
if (subArgs.length >= length) {
// 如果满足,执行函数
return fn.apply(this, subArgs);
} else {
// 如果不满足,递归返回科里化的函数,等待参数的传入
return curry.call(this, fn, subArgs);
}
};
}
// es6 实现
function curry(fn, ...args) {
return fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args);
}
【】偏函数是更加“随意”的柯里化
function getAddress(province,city){
return function (area) {
console.log(province + city + area);
}
}
let city = getAddress('浙江省','杭州市');
city('西湖区'); //浙江省杭州市西湖区
4、闭包应用——防抖、节流
【】防抖:在事件被触发n秒后执行回调,如果在这n秒内又被触发,则重新计时。使用场景多为搜索框的输入、表单数据验证等
//非立即执行版
function debounce (func, wait, ...args) {
let timeout;
return function () {
const context = this
if(timeout) clearTimeout(timeout)
timeout = setTimeout(() => {
func.apply(context, args)
}, wait)
}
}
//立即执行版
function debounce (func, wait, ...args) {
let timeout;
return function () {
const context = this;
if(timeout) clearTimeout(timeout);
let callNow = !timeout;
timeout = setTimeout(() => {
timeout = null;
}, wait)
if (callNow) func.apply(context, args)
}
}
【】节流:在一个单位时间内,只能触发一次函数,如果在这个单位时间内多次触发函数,只有一次生效。使用场景多为页面滚动监听处理等
//时间戳版(立即执行版)
function throttle (func, wait, ...args) {
let pre = 0;
return function () {
const context = this;
let now = Date.now();
if (now - pre >= wait) {
func.apply(context, args);
pre = Date.now()
}
}
}
//延时器版(非立即执行版)
function throttle (func, wait, ...args) {
let timeout;
return function () {
const context = this;
if (!timeout) {
timeout = setTimeout(() => {
timeout = null;
func.apply(context,args);
},wait)
}
}
}
5、new 操作符
【】new 对象的过程:首先创建一个新的对象,将空对象的原型地址 __proto__
指向构造函数的prototype
原型对象,在利用apply、call或bind将原本指向window的绑定对象this指向了新对象,最后返回这个新对象
function _new (func, ...args) {
const newobj = {};
newobj.__proto__ = func.prototype;
func.apply(newobj, args);
return newobj;
}
6、原型与原型链
【】每一个 JavaScript 对象(null 除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型
,每一个对象都会从原型"继承"属性,其实就是 prototype 对象
【】每个函数都有一个 prototype 属性,每个js对象都有一个 __proto__
属性,它们指向同一个地方(实例原型),即有person.__proto__ === Person.prototype
。每个原型都有一个 constructor 属性指向关联的构造函数,即有Person === Person.prototype.constructor
【】对象的 __proto__
就是所谓的原型,而原型又是一个对象,它又有自己的 __proto__
,原型的 __proto__
又是原型的原型,就这样可以一直通过 __proto__
向上找,这就是原型链,当向上找找到 Object 的原型的时候,这条原型链就算到头了,即Object.__proto__
【】原型链
:由相互关联的原型组成的链状结构就是原型链,原型链终点是Object.prototype.__proto__
,即 null 。
【】hasOwnProperty()
方法可以判断属性是否为对象非原型链上的属性
【】ES6中的类其实是一种构造函数的语法糖,它真正实现继承的方式还是原型
7、this 相关
【】函数的存储:在js中函数的存储是特别的,他是独立存储的,然后再将函数的地址指向foo,即函数不存在被指向,都是它指向别人的。因为函数是独立的,所以它可以指向不同的对象,也就是说,它可以在不同的执行上下文
var obj = { foo: function () {} };
【】立即执行函数、setTimeout 以及 setInterval 中传入的函数(非箭头函数),其中的this都指向的是全局对象 window
8、回流与重绘
【】回流这一阶段主要是计算节点的位置和几何信息,那么当页面布局和几何信息发生变化的时候,就需要回流
;当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility等),浏览器会将新样式赋予给元素并重新绘制它,这个过程重绘
而不回流;回流一定会触发重绘,而重绘不一定会回流
【】怎样减少回流和重绘:1、元素的展示隐藏尽量使用visibility。2、将对dom的操作放在一起,比如隐藏元素,修改后,重新显示。3、将对元素样式的多个操作合成一个类使用。4、使用css3硬件加速,直接不产生回流重绘(常见动画 transform)
9、async与defer
【】区别
- 都仅对外部脚本有效
- async 标志的脚本文件一旦加载完成就立即执行;而 defer 标志的脚本文件会在 HTML解析完成且DOM构建完毕后再执行
- 如果有多个js脚本,async标记的脚本哪个先下载结束,就先执行那个脚本。而defer标记则会按照js脚本书写顺序执行
- 如果同时使用async和defer属性,defer不起作用,浏览器行为由async属性决定
- DOMContentLoaded 事件会等待 defer 的脚本执行完后才触发
【】延迟加载 Js 脚本的方式还有:
- 动态创建 script 标签来引入 Js 脚本
- 使用 setTimeout 延迟加载 Js 脚本
- 让 Js 最后加载
10、浏览器线程
【】GUI 渲染线程、JS 引擎线程、定时器触发线程、浏览器事件线程、http请求线程
11、同源策略与跨域
【】同源策略是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSRF等攻击。所谓同源是指" 协议 + 域名 + 端口
"三者相同,即便两个不同的域名指向同一个ip地址,也非同源。同源策略限制内容有:Cookie、LocalStorage、IndexedDB 等存储性内容;DOM 节点;AJAX 请求发送后,结果被浏览器拦截。但是有三个标签是允许跨域加载资源:img、link、script
【】跨域解决方案:
jsonp
:利用<script>
标签没有跨域限制的漏洞,网页可以得到从其他来源动态产生的 JSON 数据。JSONP请求一定需要对方的服务器做支持才可以。缺点是仅支持get方法具有局限性,不安全可能会遭受XSS攻击cors
:CORS 需要浏览器和后端同时支持,浏览器会自动进行 CORS 通信,实现 CORS 通信的关键是后端。只要后端实现了 CORS,就实现了跨域。服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源Node中间件代理(两次跨域)
:原理是同源策略是浏览器需要遵循的标准,而如果是服务器向服务器请求就无需遵循同源策略。经过两次跨域,值得注意的是浏览器向代理服务器发送请求,也遵循同源策略。步骤:nginx反向代理
:类似于Node中间件代理,通过nginx配置一个代理服务器
12、call、apply和bind
//手写 call 函数
Function.prototype.myCall = function (context) {
// 判断调用对象
if (typeof this !== "function") {
console.error("type error")
}
// 获取参数
let args = [...arguments].slice(1)
let result = null
// 判断 context 是否传入,如果未传入则设置为 window
context = context || window
// 将调用函数设为对象的方法
context.fn = this
// 调用函数
result = context.fn(...args)
// 将属性删除
delete context.fn
return result
}
//手写 apply 函数
Function.prototype.apply = function (context) {
if (typeof this !== "function") {
console.error("type error")
}
context = context || window
let result = null
context.fn = this
if (arguments[1]) {
result = context.fn(...arguments[1])
} else {
result = context.fn()
}
delete context.fn
return result
}
// bind手写,不是最终版,最终版太难,拒绝学习~
Function.prototype.bind2 = function (context) {
var self = this;
// 获取bind2函数从第二个参数到最后一个参数
var args = Array.prototype.slice.call(arguments, 1);
return function () {
// 这个时候的arguments是指bind返回的函数传入的参数
var bindArgs = Array.prototype.slice.call(arguments);
self.apply(context, args.concat(bindArgs));
}
}
13、作用域与作用域链
【】 作用域
:规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。换句话说,作用域决定了代码区块中变量和其他资源的可见性
【】作用域链
:从当前作用域开始一层层往上找某个变量,如果找到全局作用域还没找到,就放弃寻找 。这种层级关系就是作用域链
14、数据类型判断
【】typeof
:能判断所有值类型,函数。不可对 null、对象、数组进行精确判断,因为都返回 object
【】instanceof
:能判断对象类型,不能判断基本数据类型,其内部运行机制是判断在其原型链中能否找到该类型的原型,即判断构造函数的 prototype 属性是否出现在对象的原型链中的任何位置
function myInstanceof(left, right) {
// 获取对象的原型
let proto = Object.getPrototypeOf(left)
// 获取构造函数的 prototype 对象
let prototype = right.prototype;
// 判断构造函数的 prototype 对象是否在对象的原型链上
while (true) {
if (!proto) return false;
if (proto === prototype) return true;
// 如果没有找到,就继续从其原型上找,Object.getPrototypeOf方法用来获取指定对象的原型
proto = Object.getPrototypeOf(proto);
}
}
【】constructor
:对象实例通过 constrcutor 对象访问它的构造函数
【】Object.prototype.toString.call()
:所有原始数据类型都是能判断的,还有 Error 对象,Date 对象等
15、isNaN 和 Number.isNaN 函数的区别
【】函数 isNaN 接收参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的的值都会返回 true,因此非数字值传入也会返回 true ,会影响 NaN 的判断
【】函数 Number.isNaN 会首先判断传入参数是否为数字,如果是数字再继续判断是否为 NaN ,不会进行数据类型的转换,这种方法对于 NaN 的判断更为准确
16、箭头函数与普通函数的区别
【】箭头函数比普通函数更加简洁
【】箭头函数没有自己的this。它只会在自己作用域的上一层继承this。所以箭头函数中this的指向在它在定义时已经确定了,之后不会改变。即使call()、apply()、bind()等方法也不能改变箭头函数中this的指向
【】箭头函数不能作为构造函数使用
【】箭头函数没有自己的arguments
【】箭头函数没有prototype
【】箭头函数不能用作Generator函数,不能使用yeild关键字
17、常见的类数组转换为数组的方法
【】通过 call 调用数组的 slice 方法来实现转换 Array.prototype.slice.call(arrayLike);
【】通过 call 调用数组的 splice 方法来实现转换 Array.prototype.splice.call(arrayLike, 0);
【】通过 apply 调用数组的 concat 方法来实现转换 Array.prototype.concat.apply([], arrayLike);
【】通过 Array.from 方法来实现转换 Array.from(arrayLike);
18、for…in 和 for…of 的区别
【】for…of 遍历获取的是对象的键值,for…in 获取的是对象的键名
【】for…in 循环主要是为了遍历对象而生,不适用于遍历数组;for…of 循环可以用来遍历数组、类数组对象,字符串、Set、Map 以及 Generator 对象
19、异步编程
【】异步编程的实现方式
- 回调函数:缺点是多个回调函数嵌套会造成回调地狱
- promise :解决回调地狱的问题,但是会造成多个 then 的链式调用
- generator :函数执行的过程中可以将执行权转移出去,在函数外部可以将执行权转移回来
- async 函数:是 generator 和 promise 实现的一个自动执行的语法糖,它内部自带执行器,当函数内部执行到一个 await 语句的时候,如果语句返回一个 promise 对象,那么函数将会等待 promise 对象的状态变为 resolve 后再继续向下执行
【】Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)
,有五个常用的方法:then()、catch()、all()、race()、finally
【】Promise.allSettled
:一组 Promise 实例无论成功与否,都等它们异步操作结束了在继续执行下一步操作
20、HTML5 DOM 元素类名相关操作 API classList
【】浏览器是否支持可以在控制台中运行document.body.classList
,会暴露出许多信息
【】直接暴露的信息有
- length 属性,表示元素类名的个数,只读
- item() 支持一个参数,为类名的索引,返回对应的类名
document.body.classList.item(0);
超出索引范围会返回 null - add() 支持一个类名字符串参数。表示往类名列表中新增一个类名;如果之前类名存在,则添加忽略
- remove() 支持一个类名字符串参数。表示往类名列表中移除该类名
- toggle() 支持一个类名字符串参数。无则加勉,有则移除之意。若类名列表中有此类名,移除之,并返回 false; 如果没有,则添加该类名,并返回 true
- contains() 支持一个类名字符串参数。表示往类名列表中是否包含该类名,如果包含,则返回 true, 不包含,则 false
【】就作用上讲,等同于 className
document.body.classList.toString() === document.body.className; // true
21、彻底弄清 offsetXXX、clientXXX、scrollXXX 这些属性
【】offsetWidth / offsetHeight
是指一个元素的 CSS 「标准宽高」,它包含了边框、内边距、元素内容以及滚动条(如果存在的话)
【】clientWidth / clientHeight
就表示一个元素的「内容宽高」,包含元素内容以及内边距
【】scrollWidth / scrollHeight
表示一个元素内容区域的实际大小,包括不在页面中的可滚动部分(内容和内边距)
【】offsetTop
是相对于其offsetParent元素的顶部内边距的距离——offsetParent元素:最近的包含该元素的定位元素或者最近的table,td,th,body元素
【】scrollTop
在有滚动条的情况下,为元素可视区域距离元素顶部的像素,也就是已经滚动了多少距离
22、遍历对象的七种方法
【】for ... in
:可以遍历对象的所有「可枚举属性」,包括对象本身的和对象继承来的属性
【】Object.keys()
方法可以遍历到所有对象本身的可枚举属性,但是其返回值为以遍历的属性名构成的数组
【】Object.values()
方法可以遍历到所有对象本身的可枚举属性,但是其返回的结构是以遍历的属性值构成的数组
【】Object.entries()
的返回值为Object.values()与Object.keys()的结合,也就是说它会返回一个嵌套数组,数组内包括了属性名与属性值
【】Object.getOwnPropertyNames()
遍历对象本身的所有属性(不包括Symbol()),其返回值为以遍历的属性名构成的数组
【】Object.getOwnPropertySymbols()
会返回对象内的所有Symbol属性,其返回的结构是以遍历的属性值构成的数组
【】Reflect.ownKeys()
返回的是一个大杂烩数组,即包含了对象的所有属性,无论是否可枚举还是属性是symbol,还是继承,将所有的属性返回
23、对象数组去重的方法
【】普通数组去重 [... new Set(arr)]
【】使用 filter 和 Map
function uniqueFunc(arr, uniId){
const res = new Map();
return arr.filter((item) => !res.has(item[uniId]) && res.set(item[uniId], 1));
}
【】可以去掉NaN和复杂数据类型的
function uniqueFunc(arr){
let obj = {};
return arr.filter(function (item, index, arr) {
return obj.hasOwnProperty(typeof item + JSON.stringify(item)) ? false : (obj[typeof item + JSON.stringify(item)] = true);
});
}
24、Set 和 Map有什么区别
【】Map是键值对,Set是值得集合,当然键和值可以是任何的值
【】Map可以通过get方法获取值,而set不能因为它只有值
【】都能通过迭代器进行for…of 遍历
【】Set的值是唯一的可以做数组去重,而Map由于没有格式限制,可以做数据存储
25、ES6 常见属性有哪些
【】新的变量声明:const 和 let
【】模版字符串
【】箭头函数
【】函数的参数默认值
【】扩展运算符
【】对象和数组解构赋值
【】for…of 和 for…in
【】类(原型链的语法糖表现形式)
【】对象超类
【】二进制和八进制字面量
26、document.load 与 document.DOMContentLoaded 区别
【】DOMContentLoaded:DOM解析完成即触发此事件,不等待styles, images等资源的加载
【】load:页面上所有的DOM,样式表,脚本,图片,flash都已经加载完成
【】DOMContentLoaded 绑定到 document,load 绑定到 window
document.addEventListener('DOMContentLoaded', function(event) {
console.log("DOM fully loaded and parsed"); // 先打印
});
window.addEventListener('load', function(event) {
console.log("img loaded"); // 后打印
});
27、HashRouter 和 HistoryRouter的区别和原理
【】区别
- 都是利用浏览器的两种特性实现前端路由,history是利用浏览历史记录栈的API实现,hash是监听location对象hash值变化事件来实现
- history的url没有’#'号,hash反之
- 相同的url,history会触发添加到浏览器历史记录栈中,hash不会触发
- history需要后端配合,如果后端不配合刷新新页面会出现404,hash不需要
【】原理
- HashRouter的原理:通过
window.onhashchange
方法获取新URL中hash值,再做进一步处理 - HistoryRouter的原理:通过
history.pushState
使用它做页面跳转不会触发页面刷新,使用window.onpopstate
监听浏览器的前进和后退,再做其他处理
【】总结
- hash模式下url会带有#,需要url更优雅时,可以使用history模式
- 需要兼容低版本的浏览器时,建议使用hash模式
- 需要添加任意类型数据到记录时,可以使用history模式
28、如何实现可过期的 localstorage 数据
【】惰性删除、定时删除
【】惰性删除是指某个键值过期后,该键值不会被马上删除,而是等到下次被使用的时候,才会被检查到过期,此时才能得到删除。实现方法是,存储的数据类型是个对象,该对象有两个key,一个是要存储的value值,另一个是当前时间。获取数据的时候,拿到存储的时间和当前时间做对比,如果超过过期时间就清除
【】定时删除是指,每隔一段时间执行一次删除操作,并通过限制删除操作执行的次数和频率,来减少删除操作对CPU的长期占用。另一方面定时删除也有效的减少了因惰性删除带来的对localStorage空间的浪费。实现过程,获取所有设置过期时间的key判断是否过期,过期就存储到数组中,遍历数组,每隔1S(固定时间)删除5个(固定个数),直到把数组中的key从localstorage中全部删除
【】LocalStorage清空应用场景:token存储在LocalStorage中,要清空
29、promise 手写实现
【】详细版:
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";
function Promise(excutor) {
let that = this; // 缓存当前promise实例对象
that.status = PENDING; // 初始状态
that.value = undefined; // fulfilled状态时 返回的信息
that.reason = undefined; // rejected状态时 拒绝的原因
that.onFulfilledCallbacks = []; // 存储fulfilled状态对应的onFulfilled函数
that.onRejectedCallbacks = []; // 存储rejected状态对应的onRejected函数
function resolve(value) { // value成功态时接收的终值
if(value instanceof Promise) {
return value.then(resolve, reject);
}
// 实践中要确保 onFulfilled 和 onRejected 方法异步执行,且应该在 then 方法被调用的那一轮事件循环之后的新执行栈中执行。
setTimeout(() => {
// 调用resolve 回调对应onFulfilled函数
if (that.status === PENDING) {
// 只能由pending状态 => fulfilled状态 (避免调用多次resolve reject)
that.status = FULFILLED;
that.value = value;
that.onFulfilledCallbacks.forEach(cb => cb(that.value));
}
});
}
function reject(reason) { // reason失败态时接收的拒因
setTimeout(() => {
// 调用reject 回调对应onRejected函数
if (that.status === PENDING) {
// 只能由pending状态 => rejected状态 (避免调用多次resolve reject)
that.status = REJECTED;
that.reason = reason;
that.onRejectedCallbacks.forEach(cb => cb(that.reason));
}
});
}
// 捕获在excutor执行器中抛出的异常
// new Promise((resolve, reject) => {
// throw new Error('error in excutor')
// })
try {
excutor(resolve, reject);
} catch (e) {
reject(e);
}
}
Promise.prototype.then = function(onFulfilled, onRejected) {
const that = this;
let newPromise;
// 处理参数默认值 保证参数后续能够继续执行
onFulfilled =
typeof onFulfilled === "function" ? onFulfilled : value => value;
onRejected =
typeof onRejected === "function" ? onRejected : reason => {
throw reason;
};
if (that.status === FULFILLED) { // 成功态
return newPromise = new Promise((resolve, reject) => {
setTimeout(() => {
try{
let x = onFulfilled(that.value);
resolvePromise(newPromise, x, resolve, reject); // 新的promise resolve 上一个onFulfilled的返回值
} catch(e) {
reject(e); // 捕获前面onFulfilled中抛出的异常 then(onFulfilled, onRejected);
}
});
})
}
if (that.status === REJECTED) { // 失败态
return newPromise = new Promise((resolve, reject) => {
setTimeout(() => {
try {
let x = onRejected(that.reason);
resolvePromise(newPromise, x, resolve, reject);
} catch(e) {
reject(e);
}
});
});
}
if (that.status === PENDING) { // 等待态
// 当异步调用resolve/rejected时 将onFulfilled/onRejected收集暂存到集合中
return newPromise = new Promise((resolve, reject) => {
that.onFulfilledCallbacks.push((value) => {
try {
let x = onFulfilled(value);
resolvePromise(newPromise, x, resolve, reject);
} catch(e) {
reject(e);
}
});
that.onRejectedCallbacks.push((reason) => {
try {
let x = onRejected(reason);
resolvePromise(newPromise, x, resolve, reject);
} catch(e) {
reject(e);
}
});
});
}
};
【】面试够用版:
function myPromise(constructor){
let self=this;
self.status="pending" //定义状态改变前的初始状态
self.value=undefined;//定义状态为resolved的时候的状态
self.reason=undefined;//定义状态为rejected的时候的状态
function resolve(value){
//两个==="pending",保证了状态的改变是不可逆的
if(self.status==="pending"){
self.value=value;
self.status="resolved";
}
}
function reject(reason){
//两个==="pending",保证了状态的改变是不可逆的
if(self.status==="pending"){
self.reason=reason;
self.status="rejected";
}
}
//捕获构造异常
try{
constructor(resolve,reject);
}catch(e){
reject(e);
}
}
// 定义链式调用的then方法
myPromise.prototype.then=function(onFullfilled,onRejected){
let self=this;
switch(self.status){
case "resolved":
onFullfilled(self.value);
break;
case "rejected":
onRejected(self.reason);
break;
default:
}
}
30、三种事件模型是什么?事件委托是什么?事件传播是什么
【】DOM0级模型:没有事件流的概念,不会传播; IE事件模型:有两个阶段,事件处理阶段与事件冒泡阶段; DOM2级事件模型:有三个阶段,事件捕获阶段,处理阶段,冒泡阶段
【】事件委托本质上利用了浏览器事件冒泡的机制,将子节点的监听函数定义在父节点上,由父节点的监听函数同时处理多个子元素的事件,这种方式称为事件代理
【】事件传播有三个阶段:
- 捕获阶段:事件从 window 开始,然后向下到每个元素,直到到达目标元素事件或event.target
- 目标阶段:事件已达到目标元素
- 冒泡阶段:事件从目标元素冒泡,然后上升到每个元素,直到到达window
31、关于模块化开发
【】对于模块化的理解
- 一个模块是实现一个特定功能的一组方法。通常一个文件就是一个模块,有自己的作用域,只向外暴露特定的变量和函数
- 把函数作为模块的方式会造成全局变量的污染,并且模块间没有联系
- 现在最常用的是立即执行函数的写法,通过利用闭包来实现模块私有作用域的建立,同时不会对全局作用域造成污染
- 目前流行的js模块化规范有
CommonJS、AMD、CMD以及ES6的模块系统
【】CommonJS
方案,它通过 require
来引入模块,通过 module.exports
定义模块的输出接口。这种模块加载方案是服务器端的解决方案,它是以同步
的方式来引入模块的。如果是在浏览器端,由于模块的加载是使用网络请求,因此使用异步
加载的方式更加合适,防止出现同步阻塞加载问题
【】AMD
方案 和 CMD
方案都采用异步加载的方式来加载模块。区别在于模块定义时对依赖的处理不同和对依赖模块的执行时机的处理不同
- 在模块定义时对依赖的处理不同:AMD推崇依赖前置,在定义模块的时候就要声明其依赖的模块。而 CMD 推崇就近依赖,只有在用到某个模块的时候再去 require
- 对依赖模块的执行时机处理不同:AMD 在依赖模块加载完成后就直接执行依赖模块,依赖模块的执行顺序和我们书写的顺序不一定一致。而 CMD 在依赖模块加载完成后并不执行,只是下载而已,等到所有的依赖模块都加载好后,进入回调函数逻辑,遇到 require 语句的时候才执行对应的模块,这样模块的执行顺序就和我们书写的顺序保持一致了
【】ES6 模块
的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量
【】commonJS 与 ES6 模块区别:
- CommonJS 模块输出的是一个值的拷贝,ES6 输出的是对值的引用
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口
32、手动实现数组的 map、filter、reduce 方法
function map (arr, mapCallback) {
if (!Array.isArray(arr) || !arr.length || typeof mapCallback !== "function") {
return []
} else {
let result = []
for (let i = 0; i < arr.length; i ++) {
result.push(mapCallback(arr[i]), i, arr)
}
}
return result
}
function filter (arr, filterCallback) {
if (!Array.isArray(arr) || !arr.length || typeof mapCallback !== "function") {
return []
} else {
let result = []
for (let i = 0; i < arr.length; i ++) {
if (mapCallback(arr[i], i, arr) {
result.push(arr[i])
}
}
}
return result
}
function reduce (arr, reduceCallback, initialValue) {
if (!Array.isArray(arr) || !arr.length || typeof mapCallback !== "function") {
return []
} else {
let hasInitialValue = initialValue !== undefined;
let value = hasInitialValue ? initialValue : arr[0];
for (let i = hasInitialValue ? 1 : 0, len = arr.length; i < len; i++) {
value = reduceCallback(value, arr[i], i, arr);
}
return value;
}
}
33、写一个通用的事件监听函数
const EventUtils = {
// 视能力分别使用dom0||dom2||IE方式 来绑定事件
// 添加事件
addEvent: function(element, type, handler) {
if (element.addEventListener) {
element.addEventListener(type, handler, false);
} else if (element.attachEvent) {
element.attachEvent("on" + type, handler);
} else {
element["on" + type] = handler;
}
},
// 移除事件
removeEvent: function(element, type, handler) {
if (element.removeEventListener) {
element.removeEventListener(type, handler, false);
} else if (element.detachEvent) {
element.detachEvent("on" + type, handler);
} else {
element["on" + type] = null;
}
},
// 获取事件目标
getTarget: function(event) {
return event.target || event.srcElement;
},
// 获取 event 对象的引用,取到事件的所有信息,确保随时能使用 event
getEvent: function(event) {
return event || window.event;
},
// 阻止事件(主要是事件冒泡,因为 IE 不支持事件捕获)
stopPropagation: function(event) {
if (event.stopPropagation) {
event.stopPropagation();
} else {
event.cancelBubble = true;
}
},
// 取消事件的默认行为
preventDefault: function(event) {
if (event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false;
}
}
};
34、观察者模式
【】观察者模式又称发布—订阅模式。被观察对象(subject)维护一组观察者(observer),当被观察对象状态改变时,通过调用观察者的某个方法将这些变化通知到观察者
【】用 js 手动实现观察者模式:
// 被观察者
function Subject() {
this.observers = [];
}
Subject.prototype = {
// 订阅
subscribe: function (observer) {
this.observers.push(observer);
},
// 取消订阅
unsubscribe: function (observerToRemove) {
this.observers = this.observers.filter(observer => {
return observer !== observerToRemove;
})
},
// 事件触发
fire: function () {
this.observers.forEach(observer => {
observer.call();
});
}
}
35、ES5 与 ES6 实现继承
【】javascript的继承主要是依托其原型与原型链的概念来实现的
【】ES6提供了Class关键字来实现类的定义,Class 可以通过extends关键字实现继承,让子类继承父类的属性和方法
【】ES5 实现继承:原型链继承、构造函数继承、组合式继承、寄生式组合继承
【】原型链继承:直接让子类的原型对象指向父类的实例(共享一个实例,修改数据会相互影响)
function Student() {}
Student.prototype = new Person();
Student.prototype.constructor = Student;
【】构造函数继承:子类的构造函数中执行父类的构造函数,并为其绑定子类的this(避免实例之间共享一个原型实例)
// 继承不到父类原型上的属性和方法
function Student() {
Person.apply(this, arguments);
}
【】组合式继承:将上面两种方式组合
function Student() {
Person.apply(this, arguments)
}
Student.prototype = new Person();
Student.prototype.constructor = Student;
【】寄生式组合继承:将指向父类实例改为指向父类原型, 减去一次构造函数的执行
function Person(name) {
this.name = name;
}
Person.prototype.getName = function() {
return this.name;
}
function Student() {
Person.apply(this, arguments)
}
// 原型式继承
// Student.prototype = new Person();
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;
36、什么是函数式的编程
【】这里只推荐一篇博客:简明 JavaScript 函数式编程——入门篇
37、实现一下模板字符串
let obj = { num1: "abc", num2: "def", num3: 'lmn' }
function tmp(str, obj) {
let reg = /\${(\w*)}/;
let res = reg.exec(str)
if (res) {
//eg: res[0] 匹配到 ${num1},res[1] 匹配到 num1
str = str.replace(res[0], obj[res[1]])
return tmp(str, obj)
//注意要return 递归的函数,否则打印的是undefined,
//原因是在递归函数内部没有对递归函数进行return,否则外层函数无法接收到返回值。
} else {
return str
}
}
let m = tmp("123${num1}456${num2}%", obj)
console.log(m)//123abc456def%
38、手写 promise.all
Promise.myAll = function (promises) {
let arr = [], count = 0
return new Promise((resolve, reject) => {
promises.forEach((item, i) => {
Promise.resolve(item).then(res => {
arr[i] = res
count += 1
if (count === promises.length) resolve(arr)
}).catch(reject)
})
})
}
39、手写 promise.race
Promise.myRace = function (promises) {
return new Promise ((resolve, reject) => {
for (const item of promises) {
Promise.resolve(item).then(resolve, reject)
}
})
}
40、手写 promise.any
Promise.myAny = function (promises) {
let arr = []
let count = 0
return new Promise((resolve, reject) => {
promises.forEach((item, i) => {
Promise.resolve(item).then(resolve, err => {
arr[i] = {status: 'rejected', val: err}
count += 1
if (count === promises.length) reject(new Error("没有Promise成功"))
})
})
})
}
41、实现数组的扁平
【】原生方法 flat 只能扁平一层
【】toString().split(",")
扁平出来的数组里是字符串
【】reduce 实现扁平
function func (arr) {
return arr.reduce((pre, item) => {
return pre.concat(Array.isArray(item) ? func(item) : item)
}, [])
}
42、手写实现并发控制
【】待运行任务的列表 tasks
【】同时运行任务的列表 pools
【】最大并发数(即同时运行任务的个数)max
【】流程步骤:
- 遍历待运行任务列表 tasks, 将每个任务包装为一个 Promise 对象
- 将生成的 Promise 对象存到 pools 中
- 当 pools 中的任务数量等于最大并发数 max 时, 就执行 pools 中的任务
- 当该 Promise 对象 reslove 时, 就将其从 pools 中删除
// 模拟异步请求
const request = (url) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(`任务 ${url.padEnd(10, ' ')}完成`)
}, 1000)
})
}
/**
* 并发控制函数
* @param {*} tasks
* @param {*} max
*/
async function myAsyncPool(tasks = [], max = 3) {
// 待执行任务数组
// 正在执行的任务数组
let pool = []
for (let i = 0; i < tasks.length; i++) {
// 生成异步任务
const task = request(tasks[i])
// 添加到正在执行的任务数组
pool.push(task)
task.then((data) => {
// 当任务执行完毕, 将其从正在执行任务数组中移除
console.log(`${data}; 当前并发数: ${pool.length}`)
pool.splice(pool.indexOf(task), 1)
})
// 当并发池满了, 就先去执行并发池中的任务, 有任务执行完成后, 再继续循环
if (pool.length === max) {
await Promise.race(pool)
}
}
}
const tasks = new Array(10).fill('').map((task, i) => `url - ${i + 1}`)
myAsyncPool(tasks, 3)
//每次是 3 个一组打印出来
//任务 url - 1 完成; 当前并发数: 3
//任务 url - 2 完成; 当前并发数: 3
//任务 url - 3 完成; 当前并发数: 3
//任务 url - 4 完成; 当前并发数: 3
//任务 url - 5 完成; 当前并发数: 3
//任务 url - 6 完成; 当前并发数: 3
//任务 url - 7 完成; 当前并发数: 3
//任务 url - 8 完成; 当前并发数: 3
//任务 url - 9 完成; 当前并发数: 2
//任务 url - 10 完成; 当前并发数: 1
43、手写一个方块的拖拽
【】onmousedown: 鼠标按下事件
【】onmousemove: 鼠标移动事件
【】onmouseup: 鼠标抬起事件
// 获取DOM元素
let dragDiv = document.getElementsByClassName("drag")[0];
// 鼠标按下事件 处理程序
let putDown = function (event) {
dragDiv.style.cursor = "pointer";
let offsetX = parseInt(dragDiv.style.left); // 获取当前的x轴距离
let offsetY = parseInt(dragDiv.style.top); // 获取当前的y轴距离
let innerX = event.clientX - offsetX; // 获取鼠标在方块内的x轴距
let innerY = event.clientY - offsetY; // 获取鼠标在方块内的y轴距
// 按住鼠标时为div添加一个border
dragDiv.style.borderStyle = "solid";
dragDiv.style.borderColor = "pink";
dragDiv.style.borderWidth = "3px";
// 鼠标移动的时候不停的修改div的left和top值
document.onmousemove = function (event) {
dragDiv.style.left = event.clientX - innerX + "px";
dragDiv.style.top = event.clientY - innerY + "px";
// 边界判断
if (parseInt(dragDiv.style.left) <= 0) {
dragDiv.style.left = "0px";
}
if (parseInt(dragDiv.style.top) <= 0) {
dragDiv.style.top = "0px";
}
if (parseInt(dragDiv.style.left) >= window.innerWidth - parseInt(dragDiv.style.width)) {
dragDiv.style.left = window.innerWidth - parseInt(dragDiv.style.width) + "px";
}
if (parseInt(dragDiv.style.top) >= window.innerHeight - parseInt(dragDiv.style.height)) {
dragDiv.style.top = window.innerHeight - parseInt(dragDiv.style.height) + "px";
}
}
// 鼠标抬起时,清除绑定在文档上的mousemove和mouseup事件
// 否则鼠标抬起后还可以继续拖拽方块
document.onmouseup = function () {
document.onmousemove = null;
document.onmouseup = null;
// 清除border
dragDiv.style.borderStyle = "";
dragDiv.style.borderColor = "";
dragDiv.style.borderWidth = "";
}
}
// 绑定鼠标按下事件
dragDiv.addEventListener("mousedown", putDown, false);
44、实现一个 add 方法完成两个大数相加
【】padStart()
方法用另一个字符串填充当前字符串(如果需要的话,会重复多次),以便产生的字符串达到给定的长度。从当前字符串的左侧开始填充
let a = "9007199254740991";
let b = "1234567899999999999";
function add(a ,b){
let maxLength = Math.max(a.length, b.length)
a = a.padStart(maxLength, '0')
b = b.padStart(maxLength, '0')
let f = 0
let num = ""
for (let i = maxLength - 1; i >= 0; i --) {
let t = parseInt(a[i]) + parseInt(b[i]) + f
f = Math.floor(t/10)
num = t%10 + num
}
if (f !== 0) {
num = "" + f + num
}
return num
}