谈谈JS相关原理
- 基础类型
- 变量理解
- 原型和原型链
- this指向
- 作用域
- 深浅拷贝
- 事件循环
- 异步
- 节流防抖
- WeakSet 和 WeakMap理解
- Promise
- Proxy/Reflect
- 定时器
- 闭包
基础类型
- 在 JS 中,有7 种原始值,分别是:
- boolean、null、undefined、number、string、symbol、bigint
- 引用数据类型
- 对象Object(包含普通对象
Object,数组对象Array,正则对象RegExp,日期对象Date,数学函数Math,函数对象Function)
- 对象Object(包含普通对象
null是对象吗?为什么?
结论: null不是对象。
解释: 虽然 typeof null 会输出 object,但是这只是 JS 存在的一个悠久 Bug。
null,undefined 的区别
-
undefined表示不存在这个值、或者还没有定义- 例如变量被声明了,但没有赋值时,就等于
undefined
- 例如变量被声明了,但没有赋值时,就等于
-
null表示一个对象被定义了,值为“空值”
BigInt理解
- BigInt是一种新的数据类型,可以表示一个大整数
- 之前的 JavaScript 版本中,整数的范围是有限的,超过一定范围就会失去精度,而
BigInt则可以表示任意大的整数,没有范围限制
Symbol理解
- Symbol是ES6引入了一种新的原始数据类型Symbol,表示唯一的值
何时用symbol
- 属性私有化 => 作为数据保护
- 属性使用symbol,外部就会访问不到这个属性,也就是说不能修改这个属性的值
变量理解
变量提升
- 变量提升: 变量和函数的声明会移动到全局作用域的最前面
注意: 变量提升是把变量提升到全局作用域或函数作用域的头部,不会提升变量的初始化和赋值
- 没有把变量的值提升!! 只是声明变量提升,值不提升!!!!
函数提升
- 变量可以提升,函数声明也是可以提升到顶部的
- 因此可以在函数声明之前调用函数
函数提升和变量提升优先级
在JS代码执行之前,浏览器的解析器在遇到 var 变量名 和function 整个函数 提升到当前作用域的最前面
- 函数提升优先级高于变量提升
const理解
通常用const定义常量不可以被修改,这里说的不可修改,需要理解的是,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动, const只能保证这个指向的引用指针是固定不变的,如果指向的引用地址发生变化,就会报错
- const定义的基本类型不可以修改,而对于引用类型的属性是可以修改的
- const定义的变量,而且必须初始化。否则会报错
原型和原型链
- 原型对象(Person.prototype)是 构造函数(Person)的一个实例
- 因此只有构造函数才有原型对象prototype
- 每个对象都有
__proto__原型属性 【对象里面的内部属性__proto__】 - 实例对象的原型
__proto__指向它所在的构造函数的原型对象prototype
const obj = new MyConstructor()
// 实例对象的obj的原型__proto__ 指向它所在的构建函数(MyConstructor)的原型对象prototype
原型链理解
- 当在对象实例访问属性或者方法时, 先从自身对象实例里查找,找到了直接使用,如果自身对象实例找不到,会沿着原型链向上查找, 然后去实例对象的
__proto__原型指向的它所在构造函数的原型对象里找,找到则使用,属性找不到就报undefined,方法找不到就报错- 这样一层一层向上搜索查找,就形成了原型链
原型链应用场景
- 1、数据共享,对象属性和方法的共享
- 2、扩展原生对象的功能
- 2、继承的实现
继承
- 通过原型链继承
- 构造函数继承
- 组合式继承
- 寄生式继承
- 寄生组合式继承
this指向
this是js对象中的一个特殊指针,它的指向根据环境不同而发生改变
-
对象方法调用
- 指向最后调用它的对象
-
普通函数调用
- 普通函数的this指向全局对象window
-
构造函数调用
- 构造函数中的this是指向当前对象实例的
-
apply 或 call 调用
- apply 或 call 是可以改变this的指向的,传入的对象是作为this的指向
- 如果没有传递参数,指向的是window
-
ES6中 箭头函数
- 箭头函数会捕获当前上下文的this值, 作为自己的this指向
-
DOM事件的绑定的this指向
- 指向绑定事件的元素
-
浏览器和node环境区别
- nodejs中,
this在全局作用域中不再指向全局对象,而是指向当前模块的exports对象(如果你没有导出任何内容,那么它就是一个空对象)
在nodejs模块中,export、module.exports、this会共同指向一个对象
- nodejs中,
作用域和作用域链
-
作用域:简单来说,变量的可用范围, 在代码中定义的变量区域,决定了变量的可访问性
-
作用域链:访问一个变量时,会首先在当前的作用域查找,如果在当前作用域中没有查到值,去外部环境寻找变量。一层一层的找,直到找到全局作用域执行上下文,这样的链路就是作用域链
深浅拷贝
- 浅拷贝是拷贝一层,对于引用对象的就拷贝引用;深拷贝是拷贝多层,每一级别的数据都会拷贝出来
- 浅拷贝如果数据是基本数据类型,那么就如同直接赋值那种,会拷贝其本身,(分别存储在栈内存中不同地址,不会互相影响)
- 如果除了基本数据类型之外还有一层对象,那么对于浅拷贝而言就只能拷贝对象的引用,原对象的改变会影响到拷贝对象上,因为原对象和新对象的引用类型同时指向的是堆内存
- 深拷贝就会拷贝多层,创建一个新的对象,原对象和新对象不共享堆内存地址,完全独立
实现浅拷贝的方法
- ES6中的Object.assign方法
- 展开运算符
实现深拷贝的方法
- JSON.parse(JSON.stringify())
- 会有缺点,对象里面有函数或者undefined属性会造成属性的丢失
- 递归实现
- 需要判断对象引用类型,如日期、正则
- 循环引用了就用 weakMap 来解决
事件循环
- 通过事件循环机制可以处理多个异步任务
- 保证异步任务的执行的顺序
比如2个任务操作,有了事件循环机制,就可以判断这2个事件操作,谁先执行,看哪个操作是宏任务还是微任务
异步
- 同步会阻塞代码执行,而异步不会
- js代码是从上到下执行的,如果上面代码需要执行很久,下面的代码就会被阻塞,对用户来说可能就是卡死,页面没有反应,造成很大的用户体验,因此有了异步
JS单线程
- JS是单线程的,一次只能执行一个任务
为什么是单线程?
- 避免了资源的竞争
- 如果不同的线程都执行了操作,那么会造成这个资源的竞争
- 如两个线程都都进行了DOM操作,一个添加,一个删除,那么浏览器这时候需要以哪个线程为准?
- 节省资源
- 单线程在执行时只需要占用一个线程的内存和 CPU 资源,相比多线程可以节省一些资源
节流防抖
WeakSet 和 WeakMap理解
- WeakSet 和 WeakMap: 目的是为了解决内存泄漏问题和减少内存占用。
WeakSet/WeakMap区别?
-
WeakSet和WeakMap的区别在于它们所存储的数据类型不同
-
WeakSet 的成员只能是对象,而不能是其他类型的值 【WeakSet 只有健值,没有健名,有点类似数组。】
-
WeakMap只接受对象作为键名,值可以任意类型 【WeapMap的键名所指向的对象,不计入垃圾回收机制 ,WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用。】
-
WeakSet 和 WeakMap 中存储的成员都是弱引用
- 弱引用就是不会阻止对象被垃圾回收
弱引用理解
- 1、弱引用的对象不能遍历,随时可能消失, 回收的时机是不可预测, 遍历机制无法保证成员的存在
- 如果允许遍历,那么可能会出现很诡异的现象,每次遍历的结果可能都不一样,不确定什么时候被回收了
- 3、弱引用的对象 当不在需要使用的时候,可以自动被垃圾回收,因此可以避免内存泄漏
Promise
-
可以解决回调地狱嵌套、异步编程的方案
-
Promise对象是一个构造函数,里面有all、reject、resolve方法
- Promise.reject(),Promise.resolve()..
- 在原型对象prototype中,有catch、then、finally方法
-
promise有三种状态:pending(等待),fulfilled((成功),rejected(失败)
promise特点:
- 状态一旦改变,就不会再变
- 创建promise实例后,它会立即执行, 无法中断
Proxy/Reflect
-
Proxy是创建对象的代理,可以拦截对象的操作
-
Reflect内置很多对象的操作,可以让操作对象更合理更加规范
Proxy场景
- 代理对象
- 实现响应式原理、数据绑定
- 权限控制
- 可以使用
Proxy对象来实现权限控制功能,即在对某个对象进行操作前,检查当前用户是否有相应的权限,从而避免非法操作
- 可以使用
- 数据校验
- 可以使用
Proxy对象来实现数据校验功能,即在对某个对象进行赋值操作前,对新值进行校验,如果校验不通过,则拒绝赋值
- 可以使用
setTimeout定时器
- setTimeout是异步函数定时器
- 会在当前任务队列执行完之后才会执行定时器的回调
为什么setTimeout会有4ms的延时?
- 按照 W3C 的标准,浏览器实现计时器时,如果嵌套层级超过5层,则会带有4毫秒的最少时间
setTimtout定时器的时间一定准吗?
- 不一定准,因为js是单线程的,定时器会在当前任务队列执行完之后才会执行定时器的回调
- 假如当前队列任务执行时间大于定时器设置的延迟时间,那么定时器就不是那么可靠,所以定时器时间是不准的,还要取决与当前任务队列的执行情况
如何实现一个准确的setTimeout
通过时间戳+requestAnimationFrame来实现
利用时间戳来准确计算延迟时间,并在下一次浏览器重绘之前执行回调函数,达到准确setTimeout效果
-
时间戳
- performance.now() 与页面加载有关
- Date.now() 依赖于系统的时间
-
requestAnimationFrame
-
由浏览器来决定回调函数的执行时机
-
在浏览器下一次重绘之前执行
-
闭包理解
- 闭包产生的本质:当前环境作用域中存在指向父级作用域的引用
- 闭包形成的条件:函数嵌套,内部函数引用外部函数的变量
- 闭包可以使变量长期保存在内存当中(堆内存)
闭包注意点
- 闭包使用完了后,要立即释放资源,将引用变量指向null
- 闭包所引用的变量如果没有被清除,垃圾回收无法回收,从而导致内存泄漏。
闭包的作用
闭包的两大作用:保存/保护
(1)保护:闭包可以创建一个独立的作用域,其中的变量对外部是不可见的,保护私有变量不受外界干扰
(2)保存:闭包可以保存函数内部的变量状态,当一个函数执行完毕后,如果它内部定义的变量被引用,那么这些变量的值将被保存在闭包中,不会被释放,实现了一种变量的持久化和共享
谈谈精度丢失问题
精度丢失不是js的问题,而是Number采用的是IEEE754规范中64位双精度浮点数编码, IEEE 754 标准存储位有限
js运行机制
JS的执行机制就是一个主线程 + 一个任务队列
js代码是从上到下执行的,js是单线程的,一次只能执行一个任务,对于多个异步任务的操作,有了这个事件循环,就可以处理多个异步任务,对于同步任务,会在这个主线程中执行,形成一个执行栈,而异步任务,会按照书顺序存放在任务队列中,执行栈的同步任务执行完毕,会读取任务队列中的异步任务。
内存泄漏
常见内存泄漏
- 闭包引用的变量没有清除
- 定时器没有清除
- 事件监听没有清除
- 监听窗口变化
- 监听DOM变化
- 全局变量太多未被发现
- 全局的变量是不会被垃圾回收掉的
- console.log也可能会导致内存泄漏
- 在闭包中使用console.log, 会将闭包引用的变量保持在内存中,输出的对象不会被释放
- 频繁使用console.log打印数据导致内存占用过高,从而导致内存泄漏