跳到主要内容

谈谈JS相关原理

  • 基础类型
  • 变量理解
  • 原型和原型链
  • this指向
  • 作用域
  • 深浅拷贝
  • 事件循环
  • 异步
  • 节流防抖
  • WeakSet 和 WeakMap理解
  • Promise
  • Proxy/Reflect
  • 定时器
  • 闭包

基础类型​

  • 在 JS 中,有7 种原始值,分别是:
    • boolean、null、undefined、number、string、symbol、bigint
  • 引用数据类型
    • 对象Object(包含普通对象 Object,数组对象 Array,正则对象 RegExp,日期对象 Date,数学函数 Math,函数对象Function)

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
js
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会共同指向一个对象

作用域和作用域链​

  • 作用域:简单来说,变量的可用范围, 在代码中定义的变量区域,决定了变量的可访问性

  • 作用域链:访问一个变量时,会首先在当前的作用域查找,如果在当前作用域中没有查到值,去外部环境寻找变量。一层一层的找,直到找到全局作用域执行上下文,这样的链路就是作用域链

深浅拷贝​

  • 浅拷贝是拷贝一层,对于引用对象的就拷贝引用;深拷贝是拷贝多层,每一级别的数据都会拷贝出来
  • 浅拷贝如果数据是基本数据类型,那么就如同直接赋值那种,会拷贝其本身,(分别存储在栈内存中不同地址,不会互相影响)
    • 如果除了基本数据类型之外还有一层对象,那么对于浅拷贝而言就只能拷贝对象的引用,原对象的改变会影响到拷贝对象上,因为原对象和新对象的引用类型同时指向的是堆内存
  • 深拷贝就会拷贝多层,创建一个新的对象,原对象和新对象不共享堆内存地址,完全独立

实现浅拷贝的方法​

  • 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打印数据导致内存占用过高,从而导致内存泄漏