Skip to content

【vue3js.cn】Js 系列

原文:https://vue3js.cn/interview/JavaScript/data_type.html

1. 说说JavaScript中的数据类型?存储上的差别?

在JavaScript中,我们可以分成两种类型:

  • 基本类型

  • 复杂类型

  • 两种类型的区别是:存储位置不同

基本类型主要为以下6种

  • Number
  • String
  • Boolean
  • Undefined
  • null
  • symbol

复杂类型统称为Object,我们这里主要讲述下面三种

  • Object
  • Array
  • Function
  • Date
  • Set
  • Map
  • RegExp

存储区别 基本数据类型和引用数据类型存储在内存中的位置不同:

  • 基本数据类型存储在栈中
  • 引用类型的对象存储于堆中

当我们把变量赋值给一个变量时,解析器首先要确认的就是这个值是基本类型值还是引用类型值

  • 声明变量时不同的内存地址分配:
    • 简单类型的值存放在栈中,在栈中存放的是对应的值
    • 引用类型对应的值存储在堆中,在栈中存放的是指向堆内存的地址
  • 不同的类型数据导致赋值变量时的不同:
    • 简单类型赋值,是生成相同的值,两个对象对应不同的地址
    • 复杂类型赋值,是将保存对象的内存地址赋值给另一个变量。也就是两个变量指向堆内存中同一个对象

2. 数组的常用方法有哪些?

操作方法

  • 新增
    • push()
    • unshift()
    • splice()
    • concat()
  • 删除
    • pop()
    • shift()
    • splice()
    • slice()
  • 修改
    • splice()
  • 查询
    • indexOf()
    • includes()
    • find()

排序方法 数组有两个方法可以用来对元素重新排序:

  • reverse()
  • sort()

转换方法 join()

迭代方法

  • some()
  • every()
  • forEach()
  • filter()
  • map()

3. JavaScript字符串的常用方法有哪些?

操作方法

  • 新增
    • concat()
  • 删除
    • slice()
    • substr()
    • substring()
  • 修改
    • trim()、trimLeft()、trimRight()
    • repeat()
    • padStart()、padEnd()
    • toLowerCase()、 toUpperCase()
  • 查询
  • chatAt()
  • indexOf()
  • startWith()
  • includes()

转换方法 split

模板匹配方法

  • match()
  • search()
  • replace()

4. 谈谈 JavaScript 中的类型转换机制

常见的类型转换有:

  • 强制转换(显示转换)
  • 自动转换(隐式转换)

显示转换

  • Number()
  • parseInt()
  • String()
  • Boolean()

隐式转换

  • undefined
  • null
  • false
  • +0
  • -0
  • NaN
  • ""

5. == 和 ===区别,分别在什么情况使用

等于操作符 等于操作符用两个等于号( == )表示,如果操作数相等,则会返回 true

前面文章,我们提到在JavaScript中存在隐式转换。等于操作符(==)在比较中会先进行类型转换,再确定操作数是否相等

全等操作符 全等操作符由 3 个等于号( === )表示,只有两个操作数在不转换的前提下相等才返回 true。即类型相同,值也需相同

区别 相等操作符(==)会做类型转换,再进行值的比较,全等运算符不会做类型转换

6. 深拷贝浅拷贝的区别?如何实现一个深拷贝?

浅拷贝 浅拷贝,指的是创建新的数据,这个数据有着原始数据属性值的一份精确拷贝

如果属性是基本类型,拷贝的就是基本类型的值。如果属性是引用类型,拷贝的就是内存地址

即浅拷贝是拷贝一层,深层次的引用类型则共享内存地址

在JavaScript中,存在浅拷贝的现象有:

  • Object.assign
  • Array.prototype.slice(), Array.prototype.concat()
  • 使用拓展运算符实现的复制

深拷贝 深拷贝开辟一个新的栈,两个对象属完成相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性

常见的深拷贝方式有:

  • _.cloneDeep()
  • jQuery.extend()
  • JSON.stringify()
  • 手写循环递归

7. 说说你对闭包的理解?闭包使用场景

一个函数和对其周围状态的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包

使用场景 任何闭包的使用场景都离不开这两点:

  • 创建私有变量
  • 延长变量的生命周期
  • 柯里化函数

例如计数器、延迟调用、回调等闭包的应用,其核心思想还是创建私有变量和延长变量的生命周期

8. 说说你对作用域链的理解

作用域,即变量(变量作用域又称上下文)和函数生效(能被访问)的区域或集合

换句话说,作用域决定了代码区块中变量和其他资源的可见性

我们一般将作用域分成:

  • 全局作用域
  • 函数作用域
  • 块级作用域

全局作用域 任何不在函数中或是大括号中声明的变量,都是在全局作用域下,全局作用域下声明的变量可以在程序的任意位置访问

函数作用域 函数作用域也叫局部作用域,如果一个变量是在函数内部声明的它就在一个函数作用域下面。这些变量只能在函数内部访问,不能在函数以外去访问

块级作用域 ES6引入了let和const关键字,和var关键字不同,在大括号中使用let和const声明的变量存在于块级作用域中。在大括号之外不能访问这些变量

9. JavaScript原型,原型链 ? 有什么特点?

原型 当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾

准确地说,这些属性和方法定义在Object的构造器函数(constructor functions)之上的prototype属性上,而非实例对象本身

原型链 原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain),它解释了为何一个对象会拥有定义在其他对象中的属性和方法

在对象实例和它的构造器之间建立一个链接(它是__proto__属性,是从构造函数的prototype属性派生的),之后通过上溯原型链,在构造器中找到这些属性和方法

10. Javascript如何实现继承?

继承可以使得子类具有父类别的各种属性和方法,而不需要再次编写相同的代码

在子类别继承父类别的同时,可以重新定义某些属性,并重写某些方法,即覆盖父类别的原有属性和方法,使其获得与父类别不同的功能

下面给出JavaScripy常见的继承方式:

  • 原型链继承
  • 构造函数继承(借助 call)
  • 组合继承
  • 原型式继承
  • 寄生式继承
  • 寄生组合式继承

11. 谈谈this对象的理解

函数的 this 关键字在 JavaScript 中的表现略有不同,此外,在严格模式和非严格模式之间也会有一些差别

在绝大多数情况下,函数的调用方式决定了 this 的值(运行时绑定)

this 关键字是函数运行时自动生成的一个内部对象,只能在函数内部使用,总指向调用它的对象

根据不同的使用场合,this有不同的值,主要分为下面几种情况:

  • 默认绑定
  • 隐式绑定
  • new绑定
  • 显示绑定

12. typeof 与 instanceof 区别

typeof typeof 操作符返回一个字符串,表示未经计算的操作数的类型

instanceof instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上

区别 typeof与instanceof都是判断数据类型的方法,区别如下:

  • typeof会返回一个变量的基本类型,instanceof返回的是一个布尔值
  • instanceof 可以准确地判断复杂引用数据类型,但是不能正确判断基础数据类型
  • 而typeof 也存在弊端,它虽然可以判断基础数据类型(null 除外),但是引用数据类型中,除了function 类型以外,其他的也无法判断

13. 说说new操作符具体干了什么?

从上面介绍中,我们可以看到new关键字主要做了以下的工作:

  • 创建一个新的对象obj
  • 将对象与构建函数通过原型链连接起来
  • 将构建函数中的this绑定到新建的对象obj上
  • 根据构建函数返回类型作判断,如果是原始值则被忽略,如果是返回对象,需要正常处理

14. ajax原理是什么?如何实现?

即异步的JavaScript 和XML,是一种创建交互式网页应用的网页开发技术,可以在不重新加载整个网页的情况下,与服务器交换数据,并且更新部分网页

实现 Ajax异步交互需要服务器逻辑进行配合,需要完成以下步骤:

  • 创建 Ajax的核心对象 XMLHttpRequest对象
  • 通过 XMLHttpRequest 对象的 open() 方法与服务端建立连接
  • 构建请求所需的数据内容,并通过XMLHttpRequest 对象的 send() 方法发送给服务器端
  • 通过 XMLHttpRequest 对象提供的 onreadystatechange 事件监听服务器端你的通信状态
  • 接受并处理服务端向客户端响应的数据结果
  • 将处理结果更新到 HTML页面中

15. bind、call、apply 区别?如何实现一个bind?

call、apply、bind作用是改变函数执行时的上下文,简而言之就是改变函数运行时的this指向

apply、call、bind三者的区别在于

  • 三者都可以改变函数的this对象指向
  • 三者第一个参数都是this要指向的对象,如果如果没有这个参数或参数为undefined或null,则默认指向全局window
  • 三者都可以传参,但是apply是数组,而call是参数列表,且apply和call是一次性传入参数,而bind可以分为多次传入
  • bind是返回绑定this之后的函数,apply、call 则是立即执行

16. 说说你对正则表达式的理解?应用场景?

它的设计思想是用一种描述性的语言定义一个规则,凡是符合规则的字符串,我们就认为它“匹配”了,否则,该字符串就是不合法的

应用场景 常用于表单提交时的信息校验

17. 说说你对事件循环的理解

JavaScript是一门单线程的语言,意味着同一时间内只能做一件事,但是这并不意味着单线程就是阻塞,而实现单线程非阻塞的方法就是事件循环

在JavaScript中,所有的任务都可以分为

  • 同步任务:立即执行的任务,同步任务一般会直接进入到主线程中执行
  • 异步任务:异步执行的任务,比如ajax网络请求,setTimeout定时函数等

微任务 一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前

常见的微任务有:

  • Promise.then
  • MutaionObserver
  • Object.observe(已废弃;Proxy 对象替代)
  • process.nextTick(Node.js)

宏任务 宏任务的时间粒度比较大,执行的时间间隔是不能精确控制的,对一些高实时性的需求就不太符合

常见的宏任务有:

  • script (可以理解为外层同步代码)
  • setTimeout/setInterval
  • UI rendering/UI事件
  • postMessage、MessageChannel
  • setImmediate、I/O(Node.js)

18. 说说你对BOM的理解,常见的BOM对象你了解哪些?

BOM (Browser Object Model),浏览器对象模型,提供了独立于内容与浏览器窗口进行交互的对象

常用的BOM有:

  • window
  • location
  • navigator
  • screen
  • history

19. 举例说明你对尾递归的理解,有哪些应用场景

尾递归,即在函数尾位置调用自身(或是一个尾调用本身的其他函数等等)。尾递归也是递归的一种特殊情形。尾递归是一种特殊的尾调用,即在尾部直接调用自身的递归函数

尾递归在普通尾调用的基础上,多出了2个特征

  • 在尾部调用的是函数自身
  • 可通过优化,使得计算仅占用常量栈空间

20. 说说 JavaScript 中内存泄漏的几种情况?

内存泄漏(Memory leak)是在计算机科学中,由于疏忽或错误造成程序未能释放已经不再使用的内存

并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费

垃圾回收机制 通常情况下有两种实现方式:

  • 标记清除
    • 当变量进入执行环境是,就标记这个变量为“进入环境“。进入环境的变量所占用的内存就不能释放,当变量离开环境时,则将其标记为“离开环境“
    • 垃圾回收程序运行的时候,会标记内存中存储的所有变量。然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉
  • 引用计数
    • 语言引擎有一张"引用表",保存了内存里面所有的资源(通常是各种值)的引用次数。如果一个值的引用次数是0,就表示这个值不再用到了,因此可以将这块内存释放

有了垃圾回收机制,不代表不用关注内存泄露。那些很占空间的值,一旦不再用到,需要检查是否还存在对它们的引用。如果是的话,就必须手动解除引用

常见内存泄露情况

  • 意外的全局变量
  • 定时器也常会造成内存泄露
  • 闭包,维持函数内局部变量,使其得不到释放
  • 没有清理对DOM元素的引用同样造成内存泄露
  • 包括使用事件监听addEventListener监听的时候,在不监听的情况下使用removeEventListener取消对事件监听

21. Javascript本地存储的方式有哪些?区别及应用场景?

javaScript本地缓存的方法我们主要讲述以下四种:

  • cookie
  • sessionStorage
  • localStorage
  • indexedDB 关于cookie、sessionStorage、localStorage三者的区别主要如下
  • 存储大小:cookie数据大小不能超过4k,sessionStorage和localStorage虽然也有存储大小的限制,但比cookie大得多,可以达到5M或更大
  • 有效时间:localStorage存储持久数据,浏览器关闭后数据不丢失除非主动删除数据; sessionStorage数据在当前浏览器窗口关闭后自动删除;cookie设置的cookie过期时间之前一直有效,即使窗口或浏览器关闭
  • 数据与服务器之间的交互方式,cookie的数据会自动的传递到服务器,服务器端也可以写cookie到客户端; sessionStorage和localStorage不会自动把数据发给服务器,仅在本地保存

22. 说说你对函数式编程的理解?优缺点?

主要的编程范式有三种:命令式编程,声明式编程和函数式编程

相比命令式编程,函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而非设计一个复杂的执行过程

纯函数 纯函数是对给定的输入返还相同输出的函数,并且要求你所有的数据都是不可变的,即纯函数=无状态+数据不可变

高阶函数 高级函数,就是以函数作为输入或者输出的函数被称为高阶函数

柯里化 柯里化是把一个多参数函数转化成一个嵌套的一元函数的过程

关于柯里化函数的意义如下:

  • 让纯函数更纯,每次接受一个参数,松散解耦
  • 惰性执行

组合与管道 组合函数,目的是将多个函数组合成一个函数

优缺点

  • 优点
    • 更好的管理状态:因为它的宗旨是无状态,或者说更少的状态,能最大化的减少这些未知、优化代码、减少出错情况
    • 更简单的复用:固定输入->固定输出,没有其他外部变量影响,并且无副作用。这样代码复用时,完全不需要考虑它的内部实现和外部影响
    • 更优雅的组合:往大的说,网页是由各个组件组成的。往小的说,一个函数也可能是由多个小函数组成的。更强的复用性,带来更强大的组合性
    • 隐性好处。减少代码量,提高维护性
  • 缺点:
    • 性能:函数式编程相对于指令式编程,性能绝对是一个短板,因为它往往会对一个方法进行过度包装,从而产生上下文切换的性能开销
    • 资源占用:在 JS 中为了实现对象状态的不可变,往往会创建新的对象,因此,它对垃圾回收所产生的压力远远超过其他编程方式
    • 递归陷阱:在函数式编程中,为了实现迭代,通常会采用递归操作

23. Javascript中如何实现函数缓存?函数缓存有哪些应用场景?

函数缓存,就是将函数运算过的结果进行缓存

本质上就是用空间(缓存存储)换时间(计算过程)

常用于缓存数据计算结果和缓存对象

如何实现 实现函数缓存主要依靠闭包、柯里化、高阶函数

应用场景 虽然使用缓存效率是非常高的,但并不是所有场景都适用,因此千万不要极端的将所有函数都添加缓存

以下几种情况下,适合使用缓存:

  • 对于昂贵的函数调用,执行复杂计算的函数
  • 对于具有有限且高度重复输入范围的函数
  • 对于具有重复输入值的递归函数
  • 对于纯函数,即每次使用特定输入调用时返回相同输出的函数

说说 Javascript 数字精度丢失的问题,如何解决? 理论上用有限的空间来存储无限的小数是不可能保证精确的,但我们可以处理一下得到我们期望的结果

当你拿到 1.4000000000000001 这样的数据要展示时,建议使用 toPrecision 凑整并 parseFloat 转成数字后再显示,如下

最后还可以使用第三方库,如Math.js、BigDecimal.js

24. 什么是防抖和节流?有什么区别?如何实现?

定义

  • 节流: n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效
ts
function throttled(fn, delay) {
    let timer = null
    let starttime = Date.now()
    return function () {
        let curTime = Date.now() // 当前时间
        let remaining = delay - (curTime - starttime)  // 从上一次到现在,还剩下多少多余时间
        let context = this
        let args = arguments
        clearTimeout(timer)
        if (remaining <= 0) {
            fn.apply(context, args)
            starttime = Date.now()
        } else {
            timer = setTimeout(fn, remaining);
        }
    }
}
  • 防抖: n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时
ts
function debounce(func, wait, immediate) {

    let timeout;

    return function () {
        let context = this;
        let args = arguments;

        if (timeout) clearTimeout(timeout); // timeout 不为null
        if (immediate) {
            let callNow = !timeout; // 第一次会立即执行,以后只有事件执行后才会再次触发
            timeout = setTimeout(function () {
                timeout = null;
            }, wait)
            if (callNow) {
                func.apply(context, args)
            }
        }
        else {
            timeout = setTimeout(function () {
                func.apply(context, args)
            }, wait);
        }
    }
}

区别 相同点:

  • 都可以通过使用 setTimeout 实现
  • 目的都是,降低回调执行频率。节省计算资源 不同点:
  • 函数防抖,在一段连续操作结束后,处理回调,利用clearTimeout和 setTimeout实现。函数节流,在一段连续操作中,每一段时间只执行一次,频率较高的事件中使用来提高性能
  • 函数防抖关注一定时间连续触发的事件,只在最后执行一次,而函数节流一段时间内只执行一次

25. 如何判断一个元素是否在可视区域中?

判断一个元素是否在可视区域,我们常用的有三种办法:

  • offsetTop、scrollTop
  • getBoundingClientRect
  • Intersection Observer

26. 大文件上传如何做断点续传?

分片上传 分片上传,就是将所要上传的文件,按照一定的大小,将整个文件分隔成多个数据块(Part)来进行分片上传

大致流程如下:

  • 将需要上传的文件按照一定的分割规则,分割成相同大小的数据块;
  • 初始化一个分片上传任务,返回本次分片上传唯一标识;
  • 按照一定的策略(串行或并行)发送各个分片数据块;
  • 发送完成后,服务端根据判断数据上传是否完整,如果完整,则进行数据块合成得到原始文件

断点续传 断点续传指的是在下载或上传时,将下载或上传任务人为的划分为几个部分

每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传下载未完成的部分,而没有必要从头开始上传下载。用户可以节省时间,提高速度

一般实现方式有两种:

  • 服务器端返回,告知从哪开始
  • 浏览器端自行处理

27. 如何实现上拉加载,下拉刷新?

下拉刷新和上拉加载这两种交互方式通常出现在移动端中

开源社区也有很多优秀的解决方案,如iscroll、better-scroll、pulltorefresh.js库等等

上拉刷新 判断页面触底我们需要先了解一下下面几个属性

  • scrollTop:滚动视窗的高度距离window顶部的距离,它会随着往上滚动而不断增加,初始值是0,它是一个变化的值
  • clientHeight:它是一个定值,表示屏幕可视区域的高度;
  • scrollHeight:页面不能滚动时也是存在的,此时scrollHeight等于clientHeight。scrollHeight表示body所有元素的总长度(包括body元素自身的padding)
text
scrollTop + clientHeight >= scrollHeight

下拉刷新 关于下拉刷新的原生实现,主要分成三步:

  • 监听原生touchstart事件,记录其初始位置的值,e.touches[0].pageY;
  • 监听原生touchmove事件,记录并计算当前滑动的位置值与初始位置值的差值,大于0表示向下拉动,并借助CSS3的translateY属性使元素跟随手势向下滑动对应的差值,同时也应设置一个允许滑动的最大值;
  • 监听原生touchend事件,若此时元素滑动达到最大值,则触发callback,同时将translateY重设为0,元素回到初始位置

28. 什么是单点登录?如何实现?

单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一

简单来说,一个用户只能在一个设备或者系统中登录,无法实现异地同时登录

实现方案 token令牌,与后端接口配合,当用户成功登录后更新token,使之前的token过期,并接口全部有登录鉴权的安全检测

29. web常见的攻击方式有哪些?如何防御?

我们常见的Web攻击方式有/

  • XSS (Cross Site Scripting) 跨站脚本攻击
  • CSRF(Cross-site request forgery)跨站请求伪造
  • SQL注入攻击

30. 说说var、let、const之间的区别

var、let、const三者区别可以围绕下面五点展开:

  • 变量提升
  • 暂时性死区
  • 块级作用域
  • 重复声明
  • 修改声明的变量
  • 使用

变量提升 var声明的变量存在变量提升,即变量可以在声明之前调用,值为undefined

let和const不存在变量提升,即它们所声明的变量一定要在声明后使用,否则报错

暂时性死区 var不存在暂时性死区

let和const存在暂时性死区,只有等到声明变量的那一行代码出现,才可以获取和使用该变量

块级作用域 var不存在块级作用域

let和const存在块级作用域

重复声明 var允许重复声明变量

let和const在同一作用域不允许重复声明变量

修改声明的变量 var和let可以

const声明一个只读的常量。一旦声明,常量的值就不能改变

使用 能用const的情况尽量使用const,其他情况下大多数使用let,避免使用var

31. 你是怎么理解ES6新增Set、Map两种数据结构的?

Set是一种叫做集合的数据结构,Map是一种叫做字典的数据结构

什么是集合?什么又是字典?

  • 集合
    • 是由一堆无序的、相关联的,且不重复的内存结构【数学中称为元素】组成的组合
  • 字典
    • 是一些元素的集合。每个元素有一个称作key 的域,不同元素的key 各不相同

区别

  • 共同点:集合、字典都可以存储不重复的值
  • 不同点:集合是以[值,值]的形式存储元素,字典是以[键,值]的形式存储

Released under the MIT License.