JS基础 1.数据类型 1.1基本数据类型介绍,及值类型和引用类型的理解 JS中8种基础数据类型,分别为:undefined null Boolean Number String Object Symbol BigInt
其中Symbol和BigInt是ES6新增的数据类型,可能会被单独问
Symbol代表独一无二的值,最大的用法是用来定义对象的唯一属性名
BigInt可以代表任意大小的整数
a.值类型的数值变动过程如下 let a=100;
let b = a;
a=200
console.log(b);//100
b.引用类型的赋值变动过程如下: let a = { age:20};
let b = a;
b.age=30;
console.log(a.age)//30
1.2数据类型的判断(typeof/instanceof) typeof typeof:能判断所有值类型,函数,不可对null 对象 数组进行精确判断,因为都返回object
1 2 3 4 5 6 7 8 9 10 11 12 console.log(typeof undefined)//undefined console.log(typeof 2)//number console.log(typeof true)//boolean console.log(typeof "str")//string console.log(typeof Symbol("foo"))//symbol console.log(typeof 2172141214n)//bigInt console.log(typeof function(){})//function //以下是不能判别的 console.log(typeof [])//object console.log(typeof {})//object console.log(typeof null)//object
instanceof instanceof :能判断对象类型,不能判断基本数据类型,其内部运行机制是判断在其原型链中能否找到该类型的原型。比如:
1 2 3 4 5 6 7 8 9 class People{} class Student extends People{} const vortesnail=new Student() console.log(vortesnail instanceof People)//true console.log(vortesnail instanceof Student)//true
其实就是顺着原型链去找,如果能找到对应的 xxx.prototype即为true,
1.3手写深拷贝 重点在于:判断类型(是否对象->是否特殊对象(数组))、跳出循环条件、遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 //自定义的高性能遍历方法 //@param {Array | Object} collection 要遍历的集合 //@param {Function} callback 回调函数,接收(value,key/index)参数 function forEach(collection,callback){//callback执行回调函数 if(Array.isArray(collection)){ //数组遍历 for (let i = 0;i<collection.length;i++){ callback(collection[i],i) } }else { //对象遍历-只遍历自身属性 for(let key in collection){ if (collection.hasOwnProperty(key)){ callback(collection[key],key) } } } } //深拷贝 @param {*} obj 要拷贝的值 @param {WeakMap} map 用于存储循环引用(内部使用) @returns {*} 深拷贝后的值 //deepClone主代码 function deepClone(obj, map = new WeakMap()){ //1.处理原视类型和函数 if (object === null || typeof obj !== "object"){ return obj; } //2.处理循环引用,跳出循环的关键 if (map.has(obj)){ return map.get(obj) } //3.处理特殊对象类型 //Date对象 if (obj instanceof Date){ return new Date(obj.getTime()) } //正则表达式 if (obj instanceof RegExp){ return new RegExp(obj); } //数组 if (Array.isArray(obj)){ const result = []; map.set(obj,result); forEach(obj,(value,index)=>{ result[index] = deepClone(value,map); }); return result; } //普通对象 if(obj instanceof Object){ const result = {}; map.set(obj,result); forEach(obj,(value,key)=>{ result[key]=deepClone(value,map); }); return result; } //其他未处理的情况,直接返回(如Map Set 等) return obj; } //此处为测试样例 const target = { field1: 1, field2: undefined, field3: "ConardLi", field4: { child: "child", child2: { child2: "child2" } }, field5: [1, 2,3, 4], } target.field6 = target console.log("原对象:", target); console.log("拷贝后:", deepClone(target)); console.log("是否独立对象:", target !== deepClone(target)); console.log("嵌套对象是否独立:", target.field4 !== deepClone(target).field4); }
还要考虑其他类型,比如函数(要用到 eval 和 new Function,我个人觉得这个会违反 CSP,不适合当作第三方库的处理方式)、Symbol 和 Date 等
1.4根据 0.1+0.2!==0.3,讲讲IEEE 754,如何使其相等? 原因总结:
进制转换:js在做数字计算的时候,0.1和0.2都会被转成二进制后无限循环,但是js采用的IEEE 754二进制浮点运算,最大可以存储53位有效数字,于是大于53位后面的回截掉,导致精度丢失
对阶运算:由于指数位数不相同,运算时需要对阶运算,阶小的尾数要根据阶差来右移,位数位移时可能会发生数丢失的情况,影响精度
解决办法:
1.转为整数(大数)运算 1 2 3 4 5 6 7 8 9 10 11 function add(a,b){ const maxLen = Math.max( a.toString().split(".")[1].length, b.toString().split(".")[1].length ); const base = 10 ** maxLen; const bigA = BigInt(base * a) const bigB = BigInt(base * b) const bigRes = (bigA + bigB) / BigInt(base); return Number(bigRes) }
2.使用Number.EPSILON误差范围 1 2 3 4 function isEqual(a,b){ return Math.abs(a - b) < Number.EPSILON; } console.log(isEqual(0.1+0.2,0.3))//true
3.转为字符串,对字符串做加法运算 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 //字符串数字相加 var addStrings=function(num1,num2){ //定义递归计数器 let i = num1.length -1; let j = num2.length -1; //接收结果 const res = []; //进位情况carry let carry = 0; while (i >= 0 || j>=0 ){ const n1 = i >= 0 ? Number(num1[i]) : 0; const n2 = j >= 0 ? Number(num2[j]) : 0; //当前位+上一位的进位 const sum = n1 + n2 + carry; //取个位数作为当前位的结果 res.unshift(sum % 10); //计算新的进位 carry = Math.floor(sum / 10); i--; j--; } if(carry){ res.unshift(carry);//最后还有进位要加上一位 } return res.join(""); }; function isEqual(a,b,sum){ const [intStr1,deciStr1] = a.toString().split(".");//分开整数和小数 const [intStr2,deciStr2] = b.toString().split(".");//分开整数和小数 const inteSum = addStrings(intStr1,intStr2);//获取整数相加部分 const deciSum = addStrings(deciStr1,deciStr2);//获取小数相加部分 return inteSum + "." + deciSum === String(sum); } console.log(isEqual(0.1,0.2,0.3))//true
示例:计算 “58” + “67”
text
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 初始:carry = 0 第一次循环(个位): n1 = 8, n2 = 7 sum = 8 + 7 + 0 = 15 res.unshift(15 % 10) → res = [5] carry = Math.floor(15 / 10) = 1 第二次循环(十位): n1 = 5, n2 = 6 sum = 5 + 6 + 1 = 12 res.unshift(12 % 10) → res = [2, 5] carry = Math.floor(12 / 10) = 1 循环结束: carry = 1 → res.unshift(1) → res = [1, 2, 5] 结果:"125"
2.原型和原型链 function Foo(){}
let f1= new Foo();
let f2=new Foo();
总结:
原型:每一个JavaScript对象(null除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型“继承”属性,其实就是prototype对象。
原型链:由相互关联的原型组成的链式结构就是原型链
先说出总结的话,再举例子说明如何顺着原型链找到某个属性。
推荐的阅读:JavaScript 深入之从原型到原型链 掌握基本概念,再阅读这篇文章轻松理解 JS 原型原型链 加深上图的印象。
3.作用域与作用域链 作用域:规定如何查找变量,也就是确定当前执行代码对变量的访问权限。换句话说,作用域决定了代码区块中变量和其他资源的可见性。(全局作用域、函数作用域、块级作用域)
作用域链:从当前作用域开始一层层往上找某个变量,如果找到全局作用域还没有找到,则放弃寻找。这种层级关系就是作用域链。(由多个执行上下文的变量对象构成的链表就叫做作用域链)
需要注意的是,js采用的是静态作用域,所以函数的作用域在函数定义时就确定了。
推荐阅读:先阅读JavaScript 深入之词法作用域和动态作用域 ,再阅读深入理解 JavaScript 作用域和作用域链
4.执行上下文 这部分一定要按顺序连续读这几篇文章,必须多读几遍:
总结:当JavaScript代码执行一段可执行代码时,会创建对应的执行上下文。对于每个执行上下文,都有三个重要属性:
变量对象(Variable object,VO)
作用域链(Scope chain)
this(关于this指向问题,在上面推荐的深入系列也有讲从ES规范开始讲的。可以参考这篇文章读懂:JavaScript 的 this 原理 )
5.闭包 根据MDN中文的定义,闭包的定义如下:
在JavaScript中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。可以在一个内层函数中访问到其外层函数的作用域。
闭包是指那些能够访问自由变量的函数。自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。闭包=函数+函数能够访问的自由变量
在经过上一小节“执行上下文”的学习,再来阅读这篇文章:JavaScript 深入之闭包 ,你会对闭包的实质有一定的了解:
在某个内部函数的执行上下文创建时,会将父级函数的活动对象加到内部函数的[[scope]]中,形成作用域链,所以即使父级函数的执行上下文销毁(即执行上下文栈弹出父级函数的执行上下文),但是因为其活动对象还是实际存储在内存中可被内部函数访问到的,所以实现闭包。
闭包应用:函数作为参数被传递:
1 2 3 4 5 6 7 8 9 10 11 12 function print(fn){ const a =200; fn(); } //全局变量 const a=100; function fn(){ console.log(a); } print(fn);//100
函数会记住它被创建时的词法环境
函数作为返回值被返回:
1 2 3 4 5 6 7 8 9 10 11 function create(){ const a = 100; return function(){ console.log(a); }; } const fn = create(); const a = 200; fn();//100
闭包:自由变量的查找,是在函数定义的地方,向上级作用域查找,不是在执行的地方。
应用实例:比如缓存工具、隐藏数据、只提供API
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function createCache(){ const data = {};//闭包中被隐藏的数据,不被外界访问 return { set:function(key,val){ data[key]=val; }, get:function(key){ return data[key]; }, }; } const c = createCache(); c.set("a",100) console.log(c.get("a"));//100
6.call apply bind实现 call call()方法在使用一个指定的this值和若干个指定的参数值的前提下调用某个函数或方法
1 2 3 4 5 6 7 8 var obj = { value:"vortesnail", }; function fn(){ console.log(this.value); } fn.call(obj);//vortesnail
通过call方法我们做到了以下两点:
call改变了this的指向,指向到了obj
fn函数执行了
若我们自己写call方法,可以先改造obj
1 2 3 4 5 6 7 var obj={ value:"vortesnail", fn:function(){ console.log(this.value); }, }; obj.fn();//vortesnail
这时候this就指向了obj,但是这样我们就手动给obj增加了一个fn属性,我们可以执行完再使用对象属性的删除方法就可以了
1 2 3 obj.fn = fn; obj.fn(); delete obj.fn;
根据这个思路,我们可以写出来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Function.prototype.mycall = function(context){ //判断调用对象 if(typeof this !== "function"){ throw new Error("Type error"); } //首先获取参数 let args = [...arguments].slice(1); let result = mull; //判断context是否传入,如果没有传就设置为window context = context || window; //将被调用的方法设置为context的属性 //this即为我们要调用的方法 context.fn =this //执行要被调用的方法 result = context.fn(...args); //删除手动增加的属性方法 delete context.fn; //将执行结果返回 return result; }
apply 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Function.prototype.myApply = function(context){ if(typeof this !== "function"){ throw new Error("Type error"); } let result = null; context = context || window; //与上面代码相比,我们使用Symbol来保证属性唯一 //也就是保证不会重写用户自己原来定义在context中的同名属性 const fnSymbol = Symbol(); context[fnSymbol]=this; //执行要被调用的方法 if(arguments[1]){ result=context[fnSymbol](...arguments[1]); } else { result=context[fnSymbol](); } delete context[fnSymbol]; return result; };
bind bind返回的是一个函数,这个地方可以详细阅读解析 bind 原理,并手写 bind 实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Function.prototype.myBind=function(context){ //判断调用对象是否为函数 if(typeof this!=="function"){ throw new Error("Type error"); } //获取参数 const args=[...arguments].slice[1], const fn = this; return function Fn(){ return fn.apply( this instanceof Fn ? this : context, //当前这个arguments是指Fn的参数 args.concat(...arguments) ); }; };
7.new实现(自定义new操作符实现详解) 1.首先创建一个新的空对象
2.根据原型链,设置空对象的 proto 为构建函数的prototype
3.构建函数的this指向这个对象,执行构建函数的代码(为这个新对象添加属性)
4.判断函数的返回值类型,如果是引用类型,就返回这个引用类型的对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 function _new(constructor,..args){ //1.参数验证:确保第一个参数是一个构建函数 if(typeof constructor !== 'function'){ throw new Error('constructor must be a function') } //2.创建新对象并设置原型链: //创建一个新对象,这个新对象的_proto_指向构造函数的prototype,这样就建立了原型链关系 //实现原型继承和构造函数调用 //Object.create():创建一个新对象,将这个新对象的[[Prototype]]指向指定的原型对象 const obj = Object.create(constructor.prototype) //constructor.apply():调用一个函数,将函数的this值设定为指定的对象,以数组形式传递参数 const res = constructor.apply(obj,args) //4.判断构造函数返回值的类型:处理构造函数的返回值 //如果构造函数返回对象或函数,就返回这个返回值,否则返回新创建的对象obj //细节:1.res!==null 排除null,因为typeof null === 'object'这个历史遗留问题 const isObject = typeof res === 'object' && res !== null const isFunction = typeof res === 'function' return isObject || isFunction ? res : obj } function Person(age){ this.age =age this.name='vortesnail' } const person = _new(Person,28) console.log(person)
详细细节可以看下这篇文章:# 谈谈JS中new的原理与实现
8.异步 着重理解Promise/async await/event loop
8.1 event loop 宏任务和微任务 首先推荐一个可以在线看代码流程的网站:loupe 。 然后看下这个视频学习下:到底什么是 Event Loop 呢?
Web APIs 会创建对应的线程,比如 setTimeout 会创建定时器线程,ajax 请求会创建 http 线程。。。这是由 js 的运行环境决定的,比如浏览器。
看完上面的视频之后,至少大家画 Event Loop 的图讲解不是啥问题了,但是涉及到宏任务 和微任务 ,我们还得拜读一下这篇文章:这一次,彻底弄懂 JavaScript 执行机制 。如果意犹未尽,不如再读下这篇非常详细带有大量动图的文章:做一些动图,学习一下 EventLoop 。想了解事件循环和页面渲染之间关系的又可以再阅读这篇文章:深入解析你不知道的 EventLoop 和浏览器渲染、帧动画、空闲回调(动图演示) 。
注意:1.Call Stack 调用栈空间 -> 2.尝试DOM渲染 -> 3.触发Event loop
每次Call Stack清空,即每次轮询结束,即同步任务执行完
都是DOM重新渲染的机会,DOM结构有改变则重新渲染
然后再去触发下一次Event loop
宏任务:setTimeout/setInterval/Ajax/DOM事件
微任务:Promise async/await
两者区别:
宏任务:DOM渲染后触发,如setTimeout/setInterval/DOM事件/script
微任务:DOM渲染前触发,如Promise.then/MutationObserver/Node环境下的process.nextTick
从event loop解释,为何微任务执行更早?
微任务是ES6语法规定的(被压入micro task queue)
宏任务是由浏览器规定的(通过Web APIs压入Callback queue)
宏任务执行事件一般比较长
每一次宏任务开始之前一定是伴随着一次event loop结束的,而微任务是在一次evnet loop结束前执行的
8.2 Promise 实现一次Promise A+ 规范,要知道原理
关于 Promise 的所有使用方式,可参照这篇文章:ECMAScript 6 入门 - Promise 对象 。 手写 Promise 源码的解析文章,可阅读此篇文章:从一道让我失眠的 Promise 面试题开始,深入分析 Promise 实现细节 。 关于 Promise 的面试题,可参考这篇文章:要就来 45 道 Promise 面试题一次爽到底 。
实现一个Promise.all
1 2 3 4 5 Promise.all = function(promises){ return new Promise((resolve,reject)=>{ //参数可以不是数组 }) }
8.3 async/await 和 Promise的关系 async/await是消灭异步回调的终极武器
但和Promise并不互斥,反而相辅相成
执行async函数,返回的一定是Promise对象
await相当于Promise的then
tru…catch可捕获异常,代替了Promise的catch
9 浏览器的垃圾回收机制
这里看这篇文章即可:「硬核 JS」你真的了解垃圾回收机制吗 。
有两种垃圾回收策略:
标记清除:标记阶段即为所有活动对象做上标记,清除阶段则把没有标记(也就是非活动对象)销毁
引用计数:它把对象是否不再需要简化定义为对象有么有其他对象引用到它,如果没有引用指向该对象(引用计数为0),对象将被垃圾回收机制回收。
标记清除的缺点:
内存碎片化,空间内存块是不连续的,容易出现很多空闲内存块,还可能会出现分配所需内存过大的对象时找不到合适的块。
分配速度慢,因为即便时使用First-fit策略,其操作仍是一个O(n)的操作,最坏情况是每次都要遍历到最后,同时因为碎片化,大对象的分配效率会更慢。
解决以上的缺点可以使用标记整理算法,标记结束后,标记整理算法会将活着的对象(即不需要清理的对象)向内存的一端移动,最后清理掉边界的内存。(如下图)
引用计数的缺点:
需要一个计数器,占用内存空间大,因为我们也不知道被引用数量的上限
解决不了循环引用导致的无法回收问题
V8的垃圾回收机制也是基于标记清除算法,不过对其进行了一些优化
针对新生区采用并行回收
针对老生区采用增量标记与惰性回收
10.实现一个EventMitter类
EventMitter就是发布订阅模式的典型应用:(好长)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 class EventEmitter<T extends Record<string | symbol, any>> { private eventsMap: Record<keyof T, Array<(...args: any[]) => any>> constructor() { this.eventsMap = Object.create(null) } // 触发事件 emit<K extends keyof T>(evt: K, ...args: Parameters<T[K]>) { if (!this.eventsMap[evt]) { console.warn('warn: The event not register') return false } const listeners = [...this.eventsMap[evt]] listeners.forEach((listener) => { listener(...args) }) return true } // 添加对应事件的监听函数 on<K extends keyof T>(evt: K, cb: T[K]) { if (typeof cb !== 'function') { throw new TypeError('The evet-triggered callback must be a function') } if (!this.eventsMap[evt]) { this.eventsMap[evt] = [cb] } else { this.eventsMap[evt].push(cb) } return this } // 添加一次对应事件的监听函数 once<K extends keyof T>(evt: K, cb: T[K]) { if (typeof cb !== 'function') { throw new TypeError('The evet-triggered callback must be a function') } const tempCb: any = (...args: Parameters<T[K]>) => { cb(...args) this.off(evt, tempCb) } this.on(evt, tempCb) } // 清除监听事件 off<K extends keyof T>(evt: K, cb?: T[K]) { if (!this.eventsMap[evt]) { console.warn('warn: The event not register') return } // 未传入要删除的对应回调函数,就删除注册的事件下所有监听回调函数 if (!cb) { this.eventsMap[evt].length = 0 return } if (typeof cb !== 'function') { throw new TypeError('The evet-triggered callback must be a function') } const cbs = this.eventsMap[evt] const len = cbs.length for (let i = 0; i < len; i++) { if (cbs[i] === cb) { this.eventsMap[evt].splice(i, 1) break } } } } // 测试上述 EventEmiiter 类 interface Events { add: (name: string) => void del: (name: string) => void clear: () => void } let people: Array<string> = [] const ee = new EventEmitter<Events>() ee.on('add', (name) => { people.push(name) }) ee.on('del', (name) => { people = people.filter((v) => v !== name) }) ee.once('clear', () => { people = [] }) ee.emit('add', 'vortesnail') ee.emit('add', 'kaisei') ee.emit('add', 'hcyang') console.log(people) // ['vortesnail', 'kaisei', 'hcyang'] ee.emit('del', 'kaisei') console.log(people) // ['vortesnail', 'hcyang'] // 取消了该事件 ee.off('add') ee.emit('add', 'kaisei') console.log(people) // ['vortesnail', 'hcyang'] ee.emit('clear') console.log(people) // [] // 重新添加 add 事件 ee.on('add', (name) => { people.push(name) }) ee.emit('add', 'vortesnail') ee.emit('clear') // 该事件已经无法触发 console.log(people) // ['vortesnail']