精选面试题

22 篇文章 1 订阅
18 篇文章 1 订阅

文章目录

css面试题

1.1-让元素水平居中的方法有哪些?

方法一:使用flex+margin

通过为父元素设置display: flex 子元素设置 margin: auto实现

方式二: 转成行内块, 给父盒子设置 text-align: center

方法三:使用 flex 布局

使用 flex 提供的子元素居中排列功能,对元素进行居中。

 justify-content: center;  align-items: center;

方式四: 使用定位+transform

position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);

1.2-flex:1什么意思

flex:1实际代表的是三个属性的简写

flex-grow:1 (增长)

flex-grow是用来增大盒子的,比如,当父盒子的宽度大于子盒子的宽度,父盒子的剩余空间可以利用flex-grow来设置子盒子增大的占比

flex-shrink:1 (收缩)

flex-shrink用来设置子盒子超过父盒子的宽度后,进行缩小的比例取值

flex-basis:0% (基准)

设置盒子的基准宽度,并且basis和width同时存在会把width干掉

1.3-BFC 的应用场景:

  1. 场景一:防止两个相邻块级元素的上下 margin 发生重叠 (上下 margin 合并问题)
  2. 场景二:清除浮动
  3. 场景三:实现自适应布局, 防止元素被浮动元素覆盖(左边固定, 右边自适应)

1.4-w3c标准盒模型

默认就是content-box,也就是默认标准盒模型,标准盒模型width设置了内容的宽,所以盒子实际宽度加上padding和border

JavaScript 基础

1.1-数组的常用方法有哪些?

下面前三种是对原数组产生影响的增添方法,第四种则不会对原数组产生影响

  • push()方法接收任意数量的参数,并将它们添加到数组末尾,返回数组的最新长度
  • unshift () 在数组开头添加任意多个值,然后返回新的数组长度
  • splice() 传入三个参数,分别是开始位置、0(要删除的元素数量)、插入的元素,返回空数组
  • concat() 首先会创建一个当前数组的副本,然后再把它的参数添加到副本末尾,最后返回这个新构建的数组,不会影响原始数组

下面三种都会影响原数组,最后一项不影响原数组:

  • pop() 方法用于删除数组的最后一项,同时减少数组的 length 值,返回被删除的项
  • shift()方法用于删除数组的第一项,同时减少数组的 length 值,返回被删除的项
  • splice() 传入两个参数,分别是开始位置,删除元素的数量,返回包含删除元素的数组
  • slice() 用于创建一个包含原有数组中一个或多个元素的新数组,不会影响原始数组

即修改原来数组的内容,常用splice

传入三个参数,分别是开始位置,要删除元素的数量,要插入的任意多个元素,返回删除元素的数组,对原数组产生影响

即查找元素,返回元素坐标或者元素值

  • indexOf() 返回要查找的元素在数组中的位置,如果没找到则返回 -1
  • includes() 返回要查找的元素在数组中的位置,找到返回true,否则false
  • find() 返回第一个匹配的元素

常见的转换方法有:join() 方法接收一个参数,即字符串分隔符,返回包含所有项的字符串

迭代方法

常用来迭代数组的方法(都不改变原数组)有如下:

  • some() 对数组每一项都运行传入的函数,如果有一项函数返回 true ,则这个方法返回 true
  • every() 对数组每一项都运行传入的函数,如果对每一项函数都返回 true ,则这个方法返回 true
  • forEach() 对数组每一项都运行传入的函数,没有返回值
  • filter() 对数组每一项都运行传入的函数,函数返回 true 的项会组成数组之后返回
  • map() 对数组每一项都运行传入的函数,返回由每次函数调用的结果构成的数组

1.2-bind、call、apply 区别?

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

1.3-深拷贝浅拷贝的区别?如何实现一个深拷贝?

浅拷贝

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

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

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

深拷贝

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

常见的深拷贝方式有:

  • _.cloneDeep()

  • jQuery.extend()

  • JSON.stringify() 但是这种方式存在弊端,会忽略undefinedsymbol函数

  • 手写循环递归

    // 如果是null或者undefined我就不进行拷贝操作
    // 可能是对象或者普通的值  如果是函数的话是不需要深拷贝
    // 是对象的话就要进行深拷贝
    // 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
    

    浅拷贝只复制属性指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存,修改对象属性会影响原对象

    但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象

    前提为拷贝类型为引用类型的情况下:

    • 浅拷贝是拷贝一层,属性为对象时,浅拷贝是复制,两个对象指向同一个地址
    • 深拷贝是递归拷贝深层次,属性为对象时,深拷贝是新开栈,两个对象指向不同的地址

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

一、定义
  • 节流: n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效
  • 防抖: n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时
二、区别:

相同点:

  • 都可以通过使用 setTimeout 实现
  • 目的都是,降低回调执行频率。节省计算资源

不同点:

  • 函数防抖,在一段连续操作结束后,处理回调,利用clearTimeout setTimeout实现。函数节流,在一段连续操作中,每一段时间只执行一次,频率较高的事件中使用来提高性能
  • 函数防抖关注一定时间连续触发的事件,只在最后执行一次,而函数节流一段时间内只执行一次

例如,都设置时间频率为500ms,在2秒时间内,频繁触发函数,节流,每隔 500ms 就执行一次。防抖,则不管调动多少次方法,在2s后,只会执行一次

三、应用场景

防抖在连续的事件,只需触发一次回调的场景有:

  • 搜索框搜索输入。只需用户最后一次输入完,再发送请求
  • 手机号、邮箱验证输入检测
  • 窗口大小resize。只需窗口调整完成后,计算窗口大小。防止重复渲染。

节流在间隔一段时间执行一次回调的场景有:

  • 滚动加载,加载更多或滚到底部监听
  • 搜索框,搜索联想功能

1.5-说说你对事件循环的理解

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

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

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

从上面我们可以看到,同步任务进入主线程,即主执行栈,异步任务进入任务队列,主线程内的任务执行完毕为空,会去任务队列读取对应的任务,推入主线程执行。上述过程的不断重复就事件循环

1.6-原型链和成员的查找机制

任何对象都有原型对象,也就是prototype属性,任何原型对象也是一个对象,该对象就有__proto__属性,这样一层一层往上找,就形成了一条链,我们称此为原型链;

当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。
如果没有就查找它的原型(也就是 __proto__指向的 prototype 原型对象)。
如果还没有就查找原型对象的原型(Object的原型对象)。
依此类推一直找到 Object 为止(null)。
__proto__对象原型的意义就在于为对象成员查找机制提供一个方向,或者说一条路线。

1.7-JavaScript字符串的常用方法有哪些?

一、操作方法

我们也可将字符串常用的操作方法归纳为增、删、改、查,需要知道字符串的特点是一旦创建了,就不可变

这里增的意思并不是说直接增添内容,而是创建字符串的一个副本,再进行操作

除了常用+以及${}进行字符串拼接之外,还可通过concat

concat

用于将一个或多个字符串拼接成一个新字符串

这里的删的意思并不是说删除原字符串的内容,而是创建字符串的一个副本,再进行操作

常见的有:

  • slice()
  • substr()
  • substring()

这三个方法都返回调用它们的字符串的一个子字符串,而且都接收一或两个参数。

这里改的意思也不是改变原字符串,而是创建字符串的一个副本,再进行操作

常见的有:

  • trim()、trimLeft()、trimRight()
  • repeat()
  • padStart()、padEnd()
  • toLowerCase()、 toUpperCase()
trim()、trimLeft()、trimRight()

删除前、后或前后所有空格符,再返回新的字符串

repeat()

接收一个整数参数,表示要将字符串复制多少次,然后返回拼接所有副本后的结果

padEnd()

复制字符串,如果小于指定长度,则在相应一边填充字符,直至满足长度条件

toLowerCase()、 toUpperCase()

大小写转化

除了通过索引的方式获取字符串的值,还可通过:

  • chatAt() 返回给定索引位置的字符,由传给方法的整数参数指定
  • indexOf() 从字符串开头去搜索传入的字符串,并返回位置(如果没找到,则返回 -1 )
  • startWith() 从字符串中搜索传入的字符串,并返回一个表示是否包含的布尔值
  • includes() 从字符串中搜索传入的字符串,并返回一个表示是否包含的布尔值
二、转换方法、转换方法)
split

把字符串按照指定的分割符,拆分成数组中的每一项

三、模板匹配方法

针对正则表达式,字符串设计了几个方法:

  • match() 接收一个参数,可以是一个正则表达式字符串,也可以是一个RegExp对象,返回数组
  • search() 接收一个参数,可以是一个正则表达式字符串,也可以是一个RegExp对象,找到则返回匹配索引,否则返回 -1
  • replace() 接收两个参数,第一个参数为匹配的内容,第二个参数为替换的元素(可用函数)

1.8-谈谈this对象的理解

一、定义

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

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

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

同时,this在函数执行过程中,this一旦被确定了,就不可以再更改

二、绑定规则

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

  • 默认绑定

  • 隐式绑定

  • new绑定

  • 显示绑定

    全局环境中定义person函数,内部使用this关键字

    var name = 'Jenny';
    function person() {
        return this.name;
    }
    console.log(person());  //Jenny
    

    上述代码输出Jenny,原因是调用函数的对象在游览器中位window,因此this指向window,所以输出Jenny

    注意:

    严格模式下,不能将全局对象用于默认绑定,this会绑定到undefined,只有函数运行在非严格模式下,默认绑定才能绑定到全局对象

    隐式绑定

    函数还可以作为某个对象的方法调用,这时this就指这个上级对象

    function test() {
      console.log(this.x);
    }
    
    var obj = {};
    obj.x = 1;
    obj.m = test;
    
    obj.m(); // 1
    

    这个函数中包含多个对象,尽管这个函数是被最外层的对象所调用,this指向的也只是它上一级的对象

    var o = {
        a:10,
        b:{
            fn:function(){
                console.log(this.a); //undefined
            }
        }
    }
    o.b.fn();
    

    上述代码中,this的上一级对象为bb内部并没有a变量的定义,所以输出undefined

    这里再举一种特殊情况

    var o = {
        a:10,
        b:{
            a:12,
            fn:function(){
                console.log(this.a); //undefined
                console.log(this); //window
            }
        }
    }
    var j = o.b.fn;
    j();
    

    此时this指向的是window,这里的大家需要记住,this永远指向的是最后调用它的对象,虽然fn是对象b的方法,但是fn赋值给j时候并没有执行,所以最终指向window

    new绑定

    通过构建函数new关键字生成一个实例对象,此时this指向这个实例对象

    function test() {
     this.x = 1;
    }
    
    var obj = new test();
    obj.x // 1
    

    上述代码之所以能过输出1,是因为new关键字改变了this的指向

    这里再列举一些特殊情况:

    new过程遇到return一个对象,此时this指向为返回的对象

    function fn()  
    {  
        this.user = 'xxx';  
        return {};  
    }
    var a = new fn();  
    console.log(a.user); //undefined
    

    如果返回一个简单类型的时候,则this指向实例对象

    function fn()  
    {  
        this.user = 'xxx';  
        return 1;
    }
    var a = new fn;  
    console.log(a.user); //xxx
    

    注意的是null虽然也是对象,但是此时new仍然指向实例对象

    function fn()  
    {  
        this.user = 'xxx';  
        return null;
    }
    var a = new fn;  
    console.log(a.user); //xxx
    
    显示修改

    apply()、call()、bind()是函数的一个方法,作用是改变函数的调用对象。它的第一个参数就表示改变后的调用这个函数的对象。因此,这时this指的就是这第一个参数

    var x = 0;
    function test() {
     console.log(this.x);
    }
    
    var obj = {};
    obj.x = 1;
    obj.m = test;
    obj.m.apply(obj) // 1
    
三、箭头函数

在 ES6 的语法中还提供了箭头函语法,让我们在代码书写时就能确定 this 的指向(编译时绑定)

举个例子:

const obj = {
  sayThis: () => {
    console.log(this);
  }
};

obj.sayThis(); // window 因为 JavaScript 没有块作用域,所以在定义 sayThis 的时候,里面的 this 就绑到 window 上去了
const globalSay = obj.sayThis;
globalSay(); // window 浏览器中的 global 对象

虽然箭头函数的this能够在编译的时候就确定了this的指向,但也需要注意一些潜在的坑

下面举个例子:

绑定事件监听

const button = document.getElementById('mngb');
button.addEventListener('click', ()=> {
    console.log(this === window) // true
    this.innerHTML = 'clicked button'
})

上述可以看到,我们其实是想要this为点击的button,但此时this指向了window

包括在原型上添加方法时候,此时this指向window

Cat.prototype.sayName = () => {
    console.log(this === window) //true
    return this.name
}
const cat = new Cat('mm');
cat.sayName()

同样的,箭头函数不能作为构建函数

1.9-typeof 与 instanceof 区别

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

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

可以看到,上述两种方法都有弊端,并不能满足所有场景的需求

如果需要通用检测数据类型,可以采用Object.prototype.toString,调用该方法,统一返回格式“[object Xxx]” 的字符串

2.1- 谈谈你对 JavaScript 作用域链的理解?

JavaScript 在执⾏过程中会创建一个个的可执⾏上下⽂。 (每个函数执行都会创建这么一个可执行上下文)

每个可执⾏上下⽂的词法环境中包含了对外部词法环境的引⽤,可通过该引⽤来获取外部词法环境中的变量和声明等。

这些引⽤串联起来,⼀直指向全局的词法环境,形成一个链式结构,被称为作⽤域链。

简而言之: 函数内部 可以访问到 函数外部作用域的变量, 而外部函数还可以访问到全局作用域的变量,

这样的变量作用域访问的链式结构, 被称之为作用域链

全局作用域

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

函数作用域

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

块级作用域

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

2.2-谈谈你对闭包的理解?

什么是闭包

内层函数, 引用外层函数上的变量, 就可以形成闭包

闭包让你可以在一个内层函数中访问到其外层函数的作用域

JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来,作为函数内部与外部连接起来的一座桥梁

闭包的主要作用是什么?

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

  • 创建私有变量
  • 延长变量的生命周期

一般函数的词法环境在函数返回后就被销毁,但是闭包会保存对创建时所在词法环境的引用,即便创建时所在的执行上下文被销毁,但创建时所在词法环境依然存在,以达到延长变量的生命周期的目的

2.3-如何判断是否是数组?

方法一:使用 toString 方法

方法二:使用 ES6 新增的 Array.isArray 方法

2.4-谈谈你对 this 的理解?

this 是一个在运行时才进行绑定的引用,在不同的情况下它可能会被绑定不同的对象。

默认绑定 (指向 window 的情况) (函数调用模式 fn() )

默认情况下,this 会被绑定到全局对象上,比如在浏览器环境中就为window对象,在 node.js 环境下为global对象。

隐式绑定 (谁调用, this 指向谁) (方法调用模式 obj.fn() )

如果函数的调用是从对象上发起时,则该函数中的 this 会被自动隐式绑定为对象:

显式绑定 (又叫做硬绑定) (上下文调用模式, 想让 this 指向谁, this 就指向谁)

硬绑定 => call apply bind

new 绑定 (构造函数模式)

另外,在使用 new 创建对象时也会进行 this 绑定

当使用 new 调用构造函数时,会创建一个新的对象并将该对象绑定到构造函数的 this

2.5- 箭头函数中的 this 指向什么?

箭头函数不同于传统函数,它其实没有属于⾃⼰的 this

它所谓的 this 是, 捕获其外层 上下⽂的 this 值作为⾃⼰的 this 值。

并且由于箭头函数没有属于⾃⼰的 this ,它是不能被 new 调⽤的。

我们可以通过 Babel 转换前后的代码来更清晰的理解箭头函数

2.6-Promise 的静态方法

promise 的三个状态: pending(默认) fulfilled(成功) rejected(失败)

  1. resolve 函数被执行时, 会将 promise 的状态从 pending 改成 fulfilled 成功
  2. reject 函数被执行时, 会将 promise 的状态从 pending 改成 rejected 失败

Promise.all([promise1, promise2, promise3]) 等待原则, 是在所有 promise 都完成后执行, 可以用于处理一些并发的任务

// 后面的.then中配置的函数, 是在前面的所有promise都完成后执行, 可以用于处理一些并发的任务
Promise.all([promise1, promise2, promise3]).then((values) => {
  // values 是一个数组, 会收集前面promise的结果 values[0] => promise1的成功的结果
})

Promise.race([promise1, promise2, promise3]) 赛跑, 竞速原则, 只要三个 promise 中有一个满足条件, 就会执行.then(用的较少)

2.7-宏任务 微任务 是什么

宏任务: 主线程代码, setTimeout 等属于宏任务, 上一个宏任务执行完, 才会考虑执行下一个宏任务

微任务: promise .then .catch 的需要执行的内容, 属于微任务, 满足条件的微任务, 会被添加到当前宏任务的最后去执行

执行流程
  1. 主线程先判断任务类型
    • 如果是同步任务,主线程自己执行
    • 如果是异步任务,交给宿主环境(浏览器)执行
  2. 浏览器进行异步任务的执行,每个异步执行完后,会将回调放进任务队列,先执行完成的先放进任务队列,依次放入
  3. 等主线程任务全部执行完后,发现主线程没有任务可执行了,会取任务队列中的任务,由于任务队列里是依次放入进来的,所以取得时候也会先取先进来的,也就是先进先出原则
  4. 在任务队列中取出来的任务执行完后,在取下一个,依次重复,这个过程也称为 eventLoop 事件轮训

2.8-async/await 是什么?

ES7 标准中新增的 async 函数,从目前的内部实现来说其实就是 Generator 函数的语法糖。

它基于 Promise,并与所有现存的基于 Promise 的 API 兼容。

async 关键字

  1. async 关键字用于声明⼀个异步函数(如 async function asyncTask1() {...}
  2. async 会⾃动将常规函数转换成 Promise,返回值也是⼀个 Promise 对象
  3. async 函数内部可以使⽤ await

await 关键字

  1. await 用于等待异步的功能执⾏完毕 var result = await someAsyncCall()
  2. await 放置在 Promise 调⽤之前,会强制 async 函数中其他代码等待,直到 Promise 完成并返回结果
  3. await 只能与 Promise ⼀起使⽤
  4. await 只能在 async 函数内部使⽤

2.9-相较于 Promise,async/await 有何优势?

  1. 同步化代码的阅读体验(Promise 虽然摆脱了回调地狱,但 then 链式调⽤的阅读负担还是存在的)
  2. 和同步代码更一致的错误处理方式( async/await 可以⽤成熟的 try/catch 做处理,比 Promise 的错误捕获更简洁直观)
  3. 调试时的阅读性, 也相对更友好

3.0-深拷贝 浅拷贝

引用类型, 进行赋值时, 赋值的是地址

1.浅拷贝

对数据拷贝的时候只拷贝一层,深层次的只拷贝了地址

2.深拷贝

JSON 方法实现深拷贝
var obj = {
  a: 1,
  c: {
    c1: 1,
    c2: 2
  }
}
var str = JSON.stringify(obj)
var b = JSON.parse(str)
// 修改 obj的属性
obj.c.c1 = 2
console.log(b)

我们先将需要拷贝的代码利用 JSON.stringify 转成字符转,然后再利用

JSON.parse 将字符转转回对象,即完成拷贝

问题:

  • 造成数据丢失和数据异常
  • function、undefined 直接丢失
  • NaN、Infinity 和-Infinity 变成 null
  • RegExpError对象只得到空对象;
递归深拷贝

递归实现的思路是什么样的?我们来分析一下

  1. 我们肯定要定义一个方法,那么这个方法最终应该返回一个深拷贝的数据
  2. 既然要返回一个数据,我们首先就要定义一个数据,但是数据是对象还是数组?所以需要判断,如果要拷贝的数据是数组,即定义一个数组,如果是一个对象,即定义一个对象
  3. 方法里面怎么拷贝啊?还是一样的利用 for in 循环,在循环内部,需要判断,如果是类型是简单类型,直接拷贝,如果是引用类型,就需要在一次的将引用类型里面的值取出来

但是递归也会遇到上面同样的问题

数据丢失和异常处理

处理函数 Symbol 正则 Error 等数据类型正常拷贝

// 日期格式
if (obj instanceof Date) {
  return new Date(obj)
}
// Symbol
if (obj instanceof Symbol) {
  return new Symbol(obj)
}
// 函数
if (obj instanceof Function) {
  return new Function(obj)
}
// 正则
if (obj instanceof RegExp) {
  return new RegExp(obj)
}		
循环引用问题

数据自己引用自己,此时拷贝就会进入死循环

解决思路:

将每次拷贝的数据进行存储,每次在拷贝之前,先看该数据是否拷贝过,如果拷贝过,直接返回,不在拷贝,如果没有拷贝,对该数据进行拷贝并记录该数据以拷贝

1、使用数组

// 要是有 别再循环拷贝了 直接返回 该值
// 数据不存在,保存源数据,以及对应的引用
// 新增方法,用于查找

2、使用 map 数据:强引用,无法被垃圾回收

// 读取要拷贝的数据
// 要是有 别再循环拷贝了 直接返回 该值
// 存拷贝的数据

3、使用 hash 表:弱引用,可被垃圾回收

// 读取要拷贝的数据 // 要是有 别再循环拷贝了 直接返回 该值
// 新增代码,哈希表设值
// 新增代码,传入哈希表

使用 lodash 实现深拷贝

3.1-常见状态码

304表示,客户端有缓存文件并向服务器发送了一个options请求,服务器返回304 Not Modified,告诉客户端,原来缓存的文件没有修改过,可以继续使用原来缓存的文件。

403: ‘得到访问授权,但访问是被禁止’,

404: ‘访问的是不存在的资源’,

500: ‘服务器不可用,未返回正确的数据’,

3.2-重绘(repaint)和重排(回流 reflow)是什么?

重排

重排是指部分或整个渲染树需要重新分析,并且节点的尺⼨需要重新计算。

表现为 重新⽣成布局,重新排列元素。

重绘

重绘是由于节点的⼏何属性发⽣改变,或由于样式发⽣改变(例如:改变元素背景⾊)。

表现为某些元素的外观被改变。或者重排后, 进行重新绘制!

两者的关系

重绘不⼀定会出现重排,重排必定会触发重绘。

每个页面至少需要一次回流+重绘。(初始化渲染)

重排和重绘的代价都很⾼昂,频繁重排重绘, 会破坏⽤户体验、让界面显示变迟缓。

我们需要尽可能避免频繁触发重排和重绘, 尤其是重排

3.3-何时会触发重排?

重排什么时候发生?

1、添加或者删除可见的 DOM 元素;

2、元素位置改变;

3、元素尺寸改变——边距、填充、边框、宽度和高度

4、内容改变——比如文本改变或者图片大小改变而引起的计算值宽度和高度改变;

5、页面渲染初始化;

6、浏览器窗口尺寸改变——resize 事件发生时;

3.4-如何实现跨域获取数据?

历史上出现过的跨域⼿段有很多,主要了解 3 种跨域⽅案:

  • JSONP
  • CORS
  • 服务器代理(webpack 代理, Nginx 反向代理)

JSONP

这是一种非常经典的跨域方案,它利用了<script> 标签不受同源策略的限制的特性,实现跨域效果。

优点:

  • 实现简单
  • 兼容性好

缺点:

  • 只支持 GET 请求 (因为 <script> 标签只能发送 GET 请求)
  • 存在被 XSS 攻击的可能,缺乏安全性保证
  • 需要服务端配合改造

axios 中不支持 JSONP, 如果在开发中, 需要发送 JSONP 请求, 可以用 jsonp 插件

CORS (主流)

跨域资源共享(CORS),这是⽬前比较主流的跨域解决⽅案,

它利用一些额外的 HTTP 响应头来通知浏览器, 允许访问来自指定 origin 的非同源服务器上的资源。

Node.js 的 Express 框架的设置代码 (Java, PHP 等, 配置代码差不多):

// 创建一个 CORS 中间件
// 为 Express 配置 CORS 中间件

优先让后台配置个 CORS 解决即可, 简单快捷!

代理服务器

说明: 同源策略, 是浏览器的安全策略, 服务器于服务器之间, 没有跨域问题! 所以可以利用代理服务器转发请求!

  1. 开发环境的跨域问题 (使用 webpack 代理服务器解决)

    配置 devServer 的 proxy 配置项

module.exports = {
  devServer: {
    // 代理配置
    proxy: {
      // 这里的api 表示如果我们的请求地址有/api的时候,就出触发代理机制
      '/api': {
        target: 'www.baidu.com', // 我们要代理请求的地址
        // 路径重写
        pathRewrite: {
          // 路径重写  localhost:8888/api/login  => www.baidu.com/api/login
          '^/api': '', // 假设我们想把 localhost:8888/api/login 变成www.baidu.com/login 就需要这么做
        },
      },
    },
  },
}

生产环境的跨域问题 (使用 nginx 服务器代理)

前端工程化

1.1-Babel 的原理是什么?

Babel 的主要工作是对代码进行转译。(解决兼容, 解析执行一部分代码)

转译分为三阶段:

  • 解析(Parse),将代码解析⽣成抽象语法树 AST,也就是词法分析与语法分析的过程
  • 转换(Transform),对语法树进⾏变换方面的⼀系列操作。通过 babel-traverse,进⾏遍历并作添加、更新、删除等操作
  • ⽣成(Generate),通过 babel-generator 将变换后的 AST 转换为 JS 代码

Vue

1.1- 什么是 M V VM

Model 层: 数据模型层

  • 通过 Ajaxfetch 等 API 完成客户端和服务端业务模型的同步。

View 层: 视图层

  • 作为视图模板存在,其实 View 就是⼀个动态模板。

ViewModel 层: 视图模型层

  • 负责暴露数据给 View 层,并对 View 层中的数据绑定声明、 指令声明、 事件绑定声明, 进行实际的业务逻辑实现。

数据变化了, 视图自动更新 => ViewModel 底层会做好监听 Object.defineProperty,当数据变化时,View 层会自动更新

视图变化了, 绑定的数据自动更新 => 会监听双向绑定的表单元素的变化,⼀旦变化,绑定的数据也会得到⾃动更新。

1.2-Vuex

  • 全局状态管理库。可通过它来进行全局数据流的管理。
  • state: 存放数据
  • mutations: 存放操作数据的方法
  • actions: 存放一些异步操作 (也可以进行一些同步处理) 注意: actions 是不能直接修改 state 数据的, 需要提交 mutation
  • getters: 存放基于 state 计算出来的一些值 (计算属性)
  • modules: 分模块, 项目大了, 也推荐分模块管理 (同模块的 vuex 操作, 就会在一起)
  • 注意点: 分模块了, 默认 muations, actions, getters 注册到全局的, 一般会开启命名空间
  • 语法: namespaced: true

1.3-computed 和 watch 的区别是什么?

computed

  1. 它是计算属性。主要用于值的计算并一般会返回一个值。所以它更多⽤于计算值的场景
  2. 它具有缓存性。当访问它来获取值时,它的 getter 函数所计算出来的值会进行缓存
  3. 只有当它依赖的属性值发生了改变,那下⼀次再访问时才会重新调⽤ getter 函数来计算
  4. 它适⽤于计算⽐较消耗性能的计算场景
  5. 必须要有一个返回值

watch

  1. 它更多的是起到 “观察” 的作⽤,类似于对数据进行变化的监听并执行回调。

    主要⽤于观察 props 或 本组件 data 的值,当这些值发生变化时,执⾏处理操作

  2. 不一定要返回某个值

建议

  1. 当目的是进⾏数值计算,且依赖于其他数据,那么推荐使用 computed
  2. 当需要在某个数据发生变化的, 同时做⼀些稍复杂的逻辑操作,那么推荐使⽤ watch

1.4-keep-alive的使用场景跟原理分析

  1. vue自带的组件 >> 主要功能是缓存组件 >> 提升性能
  2. 使用场景:可以少网络请求,如果当前组件数据量比较大,就可以节省网络请求 >> 提升用户体验
  3. 举例:如果详情页面之间进行切换,就可以使用keep-alive进行缓存组件,防止同样的数据重复请求
1.4.1-Keep-alive 是什么

keep-alivevue中的内置组件,能在组件切换过程中将状态保留在内存中,防止重复渲染DOM

keep-alive 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们

keep-alive可以设置以下props属性:

  • include - 字符串或正则表达式。只有名称匹配的组件会被缓存
  • exclude - 字符串或正则表达式。任何名称匹配的组件都不会被缓存
  • max - 数字。最多可以缓存多少组件实例
1.4.2-使用场景

使用原则:当我们在某些场景下不需要让页面重新加载时我们可以使用keepalive

举个栗子:

当我们从首页–>列表页–>商详页–>再返回,这时候列表页应该是需要keep-alive

首页–>列表页–>商详页–>返回到列表页(需要缓存)–>返回到首页(需要缓存)–>再次进入列表页(不需要缓存),这时候可以按需来控制页面的keep-alive

在路由中设置keepAlive属性判断是否需要缓存

1.5-Vue 中的 key

一句话来讲

key是给每一个vnode的唯一id,也是diff的一种优化策略,可以根据key,更准确, 更快的找到对应的vnode节点

场景背后的逻辑

当我们在使用v-for时,需要给单元加上key

  • 如果不用key,Vue会采用就地复地原则:最小化element的移动,并且会尝试尽最大程度在同适当的地方对相同类型的element,做patch或者reuse。
  • 如果使用了key,Vue会根据keys的顺序记录element,曾经拥有了key的element如果不再出现的话,会被直接remove或者destoryed
  • 当拥有新值的rerender作为key时,拥有了新key的Comp出现了,那么旧key Comp会被移除,新key Comp触发渲染
key 的常见应用场景

key 的常见应用场景 => v-for, v-for 遍历的列表中的项的顺序, 非常的容易改变

1 往后面加, 默认的对比策略, 按照下标, 没有任何问题

2 往前面加, 由于下标变了, 如果按照之前的下标对比, 元素是混乱的, 策略: 加上 key

一旦加上了 key, 就是按照 key 进行新旧 dom 的对比了

总结: key 就是给 虚拟 dom 添加了一个 标识, 优化了对比策略!!!

1.6- Vue 项目权限处理

现在权限相关管理系统用的框架都是 element 提供的vue-element-admin模板框架比较常见。

权限控制常见分为三大块

  • 菜单权限控制
  • 按钮权限控制
  • 请求 url 权限控制。

权限管理在后端中主要体现在对接口访问权限的控制,在前端中主要体现在对菜单访问权限的控制。

  1. 按钮权限控制比较容易,主要采取的方式是从后端返回按钮的权限标识,然后在前端进行显隐操作 v-if / disabled。

  2. url 权限控制,主要是后端代码来控制,前端只需要规范好格式即可。

  3. 剩下的菜单权限控制,是相对复杂一些的

    (1) 需要在路由设计时, 就拆分成静态路由和动态路由

    静态路由: 所有用户都能访问到的路由, 不会动态变化的 (登录页, 首页, 404, …)

    动态路由: 动态控制的路由, 只有用户有这个权限, 才将这个路由添加给你 (审批页, 社保页, 权限管理页…)

    (2) 用户登录进入首页时, 需要立刻发送请求, 获取个人信息 (包含权限的标识)

    (3) 利用权限信息的标识, 筛选出合适的动态路由, 通过路由的 addRoutes 方法, 动态添加路由即可!

    (4) router.options.routes (拿的是默认配置的项, 拿不到动态新增的) 不是响应式的!

    为了能正确的显示菜单, 为了能够将来正确的获取到用户路由, 我们需要用vuex 管理 routes 路由数组

    (5) 利用 vuex 中的 routes, 动态渲染菜单

1.7- 如何处理 打包出来的项目(首屏)加载过慢的问题

SPA 应用: 单页应用程序, 所有的功能, 都在一个页面中, 如果第一次将所有的路由资源, 组件都加载了, 就会很慢!

加载过慢 => 一次性加载了过多的资源, 一次性加载了过大的资源

  • 加载过多 => 路由懒加载, 访问到路由, 再加载该路由相关的组件内容
  • 加载过大 => 图片压缩, 文件压缩合并处理, 开启 gzip 压缩等

比如:

  1. 配置异步组件, 路由懒加载
  2. 图片压缩: 使用 webp 格式的图片, 提升首页加载的速度
  3. CDN 加速: 配置 CDN 加速, 加快资源的加载效率 (花钱)
  4. 开启 gzip 压缩 (一般默认服务器开启的, 如果没开, 确实可能会很慢, 可以让后台开一下)

1.8-你在项目中遇到过什么技术难题

1: 如果之前没有做过国际化, 换肤, 没有做过支付, 权限控制, 没有做过即时通信 websocket, excel 导入导出, 就会觉得很难,

但其实真正上手花时间去学着做了, 也都能逐步思考解决相关页面, 这些其实也都还 ok

比如 2: 有时候, 复杂的或者困难的, 并不是技术层面的, 而是业务需求方面的, 需要进行大量树形结构的处理

展示列表式数据时, 展示图表数据时, 筛选条件关联条件多了, 组件与组件的联动关系的控制也比较麻烦,

将联动的条件, 存 vuex, 然后 => 进行分模块管理也是比较合适的选择

2- Vue 双向绑定原理?

2.1 基本认知

在 Vue 2.x 中,利⽤的是 Object.defineProperty 去劫持对象的访问器(Getter、Setter),

当对象属性值发⽣变化时可获取变化,然后根据变化来作后续响应;(一个一个的劫持)

在 Vue 3.0 中,则是通过 Proxy 代理对象进⾏类似的操作。劫持的是整个对象, 只要对象中的属性变化了, 都能劫持到

2.2 Object.defineProperty 和 Proxy 的优缺点?

Proxy

  • 可以直接监听整个对象,⽽⾮是对象的某个属性

  • 可以直接监听数组的变化

  • 拦截⽅法丰富:多达 13 种,不限于get set deletePropertyhas 等。

    Object.defineProperty 强大很多

Object.defineProperty

  • 兼容性较好(可⽀持到 IE9)

3. 如何理解 Vue 的响应式系统?

(考察 MVVM) M: model 数据模型, V:view 视图模型, VM: viewModel 视图数据模型

双向:

  1. 视图变化了, 数据自动更新 => 监听原生的事件即可, 输入框变了, 监听输入框 input 事件
  2. 数据变化了, 视图要自动更新 => vue2 和 vue3
3.1- 基本原理

vue2.0 数据劫持: Object.defineProperty (es5)

vue3.0 数据劫持: Proxy (es6)

分析 :此题考查 Vue 的 MVVM 原理

解答: Vue 的双向绑定原理其实就是 MVVM 的基本原理, Vuejs 官网已经说明, 实际就是通过 Object.defineProperty 方法 完成了对于 Vue 实例中数据的 劫持, 通过对于 data 中数据 进行 set 的劫持监听, 然后通过**观察者模式**, 通知 对应的绑定节点 进行节点数据更新, 完成数据驱动视图的更新

简单概述 : 通过 Object.defineProperty 完成对于数据的劫持, 通过观察者模式, 完成对于节点的数据更新

3.2- 观察者模式

观察者模式: 当对象间存在 一对多 关系时,则使用观察者模式(Observer Pattern)。

比如,当一个对象或者数据被修改时,则会自动通知依赖它的对象。

3.3-如何实现上拉加载,下拉刷新?

一、前言

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

本质上等同于PC网页中的分页,只是交互形式不同

开源社区也有很多优秀的解决方案,如iscrollbetter-scrollpulltorefresh.js库等等

这些第三方库使用起来非常便捷

我们通过原生的方式实现一次上拉加载,下拉刷新,有助于对第三方库有更好的理解与使用

二、实现原理
上拉加载

上拉加载及下拉刷新都依赖于用户交互

最重要的是要理解在什么场景,什么时机下触发交互动作

上拉加载的本质是页面触底,或者快要触底时的动作

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

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

综上我们得出一个触底公式:

scrollTop + clientHeight >= scrollHeight
下拉刷新

下拉刷新的本质是页面本身置于顶部时,用户下拉时需要触发的动作

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

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

从上面可以看到,在下拉到松手的过程中,经历了三个阶段:

  • 当前手势滑动位置与初始位置差值大于零时,提示正在进行下拉刷新操作
  • 下拉到一定值时,显示松手释放后的操作提示
  • 下拉到达设定最大值松手时,执行回调,提示正在进行更新操作

3.4-如何实现jwt鉴权机制?说说你的思路

一、是什么

JWT(JSON Web Token),本质就是一个字符串书写规范,如下图,作用是用来在用户和服务器之间传递安全可靠的信息

在目前前后端分离的开发过程中,使用token鉴权机制用于身份验证是最常见的方案,流程如下:

  • 服务器当验证用户账号和密码正确的时候,给用户颁发一个令牌,这个令牌作为后续用户访问一些接口的凭证
  • 后续访问会根据这个令牌判断用户时候有权限进行访问

Token,分成了三部分,头部(Header)、载荷(Payload)、签名(Signature),并以.进行拼接。其中头部和载荷都是以JSON格式存放数据,只是进行了编码

二、如何实现

Token的使用分成了两部分:

  • 生成token:登录成功的时候,颁发token
  • 验证token:访问某些资源或者接口时,验证token
生成 token

借助第三方库jsonwebtoken,通过jsonwebtokensign 方法生成一个 token

  • 第一个参数指的是 Payload
  • 第二个是秘钥,服务端特有
  • 第三个参数是 option,可以定义 token 过期时间

在前端接收到token后,一般情况会通过localStorage进行缓存,然后将token放到HTTP 请求头Authorization 中,关于Authorization 的设置,前面要加上 Bearer ,注意后面带有空格

校验token

使用 koa-jwt 中间件进行验证,方式比较简单

  • secret 必须和 sign 时候保持一致
  • 可以通过 unless 配置接口白名单,也就是哪些 URL 可以不用经过校验,像登陆/注册都可以不用校验
  • 校验的中间件需要放在需要校验的路由前面,无法对前面的 URL 进行校验

注意:上述的HMA256加密算法为单秘钥的形式,一旦泄露后果非常的危险

在分布式系统中,每个子系统都要获取到秘钥,那么这个子系统根据该秘钥可以发布和验证令牌,但有些服务器只需要验证令牌

这时候可以采用非对称加密,利用私钥发布令牌,公钥验证令牌,加密算法可以选择RS256

三、优缺点

优点:

  • json具有通用性,所以可以跨语言
  • 组成简单,字节占用小,便于传输
  • 服务端无需保存会话信息,很容易进行水平扩展
  • 一处生成,多处使用,可以在分布式系统中,解决单点登录问题
  • 可防护CSRF攻击

缺点:

  • payload部分仅仅是进行简单编码,所以只能用于存储逻辑必需的非敏感信息
  • 需要保护好加密密钥,一旦泄露后果不堪设想
  • 为避免token被劫持,最好使用https协议

3.5-说说你对发布订阅、观察者模式的理解?区别?

一、观察者模式

观察者模式定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知,并自动更新

观察者模式属于行为型模式,行为型模式关注的是对象之间的通讯,观察者模式就是观察者和被观察者之间的通讯

例如生活中,我们可以用报纸期刊的订阅来形象的说明,当你订阅了一份报纸,每天都会有一份最新的报纸送到你手上,有多少人订阅报纸,报社就会发多少份报纸

报社和订报纸的客户就形成了一对多的依赖关系

二、发布订阅模式

发布-订阅是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者)。而是将发布的消息分为不同的类别,无需了解哪些订阅者(如果有的话)可能存在

同样的,订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者存在

三、区别

两种设计模式思路是一样的,举个生活例子:

  • 观察者模式:某公司给自己员工发月饼发粽子,是由公司的行政部门发送的,这件事不适合交给第三方,原因是“公司”和“员工”是一个整体
  • 发布-订阅模式:某公司要给其他人发各种快递,因为“公司”和“其他人”是独立的,其唯一的桥梁是“快递”,所以这件事适合交给第三方快递公司解决

上述过程中,如果公司自己去管理快递的配送,那公司就会变成一个快递公司,业务繁杂难以管理,影响公司自身的主营业务,因此使用何种模式需要考虑什么情况两者是需要耦合的

两者区别如下图:

img

  • 在观察者模式中,观察者是知道Subject的,Subject一直保持对观察者进行记录。然而,在发布订阅模式中,发布者和订阅者不知道对方的存在。它们只有通过消息代理进行通信。
  • 在发布订阅模式中,组件是松散耦合的,正好和观察者模式相反。
  • 观察者模式大多数时候是同步的,比如当事件触发,Subject就会去调用观察者的方法。而发布-订阅模式大多数时候是异步的(使用消息队列)

3.6-你是怎么理解ES6中 Promise的?使用场景?

Promise ,译为承诺,是异步编程的一种解决方案,解决回调地狱比传统的解决方案(回调函数)更加合理和更加强大

状态

promise对象仅有三种状态

  • pending(进行中)
  • fulfilled(已成功)
  • rejected(已失败)
特点
  • 对象的状态不受外界影响,只有异步操作的结果,可以决定当前是哪一种状态
  • 一旦状态改变(从pending变为fulfilled和从pending变为rejected),就不会再变,任何时候都可以得到这个结果
实例方法

Promise构建出来的实例存在以下方法:

  • then()

    then是实例状态发生改变时的回调函数,第一个参数是resolved状态的回调函数,第二个参数是rejected状态的回调函数

    then方法返回的是一个新的Promise实例,也就是promise能链式书写的原因

  • catch()

    catch()方法是.then(null, rejection).then(undefined, rejection)的别名,用于指定发生错误时的回调函数

    Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止

    一般来说,使用catch方法代替then()第二个参数

    Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应

    catch()方法之中,还能再抛出错误,通过后面catch方法捕获到

  • finally()

    finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作

构造函数方法

Promise构造函数存在以下方法:

  • all()

    Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例

    接受一个数组(迭代对象)作为参数,数组成员都应为Promise实例

    实例p的状态由p1p2p3决定,分为两种:

    • 只有p1p2p3的状态都变成fulfilledp的状态才会变成fulfilled,此时p1p2p3的返回值组成一个数组,传递给p的回调函数
    • 只要p1p2p3之中有一个被rejectedp的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数

    注意,如果作为参数的 Promise 实例,自己定义了catch方法,那么它一旦被rejected,并不会触发Promise.all()catch方法

  • race()

    Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例

    只要p1p2p3之中有一个实例率先改变状态,p的状态就跟着改变

    率先改变的 Promise 实例的返回值则传递给p的回调函数

  • allSettled()

    Promise.allSettled()方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例

    只有等到所有这些参数实例都返回结果,不管是fulfilled还是rejected,包装实例才会结束

  • resolve()

    将现有对象转为 Promise 对象

    参数可以分成四种情况,分别如下:

    • 参数是一个 Promise 实例,promise.resolve将不做任何修改、原封不动地返回这个实例
    • 参数是一个thenable对象,promise.resolve会将这个对象转为 Promise 对象,然后就立即执行thenable对象的then()方法
    • 参数不是具有then()方法的对象,或根本就不是对象,Promise.resolve()会返回一个新的 Promise 对象,状态为resolved
    • 没有参数时,直接返回一个resolved状态的 Promise 对象
  • reject()

    Promise.reject(reason)`方法也会返回一个新的 Promise 实例,该实例的状态为`rejected
    Promise.reject()方法的参数,会原封不动地变成后续方法的参数
    
使用场景

将图片的加载写成一个Promise,一旦加载完成,Promise的状态就发生变化

通过链式操作,将多个渲染数据分别给个then,让其各司其职。或当下个异步请求依赖上个请求结果的时候,我们也能够通过链式操作友好解决问题

通过all()实现多个请求合并在一起,汇总所有请求结果,只需设置一个loading即可

通过race可以设置图片请求超时

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

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

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

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

区别?

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

3.8-说说var、let、const之间的区别

一、var

在ES5中,顶层对象的属性和全局变量是等价的,用var声明的变量既是全局变量,也是顶层变量

注意:顶层对象,在浏览器环境指的是window对象,在 Node 指的是global对象

var a = 10;
console.log(window.a) // 10

使用var声明的变量存在变量提升的情况

console.log(a) // undefined
var a = 20

在编译阶段,编译器会将其变成以下执行

var a
console.log(a)
a = 20

使用var,我们能够对一个变量进行多次声明,后面声明的变量会覆盖前面的变量声明

var a = 20 
var a = 30
console.log(a) // 30

在函数中使用使用var声明变量时候,该变量是局部的

var a = 20
function change(){
    var a = 30
}
change()
console.log(a) // 20 

而如果在函数内不使用var,该变量是全局的

var a = 20
function change(){
   a = 30
}
change()
console.log(a) // 30 
二、let

letES6新增的命令,用来声明变量

用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效

{
    let a = 20
}
console.log(a) // ReferenceError: a is not defined.

不存在变量提升

console.log(a) // 报错ReferenceError
let a = 2

这表示在声明它之前,变量a是不存在的,这时如果用到它,就会抛出一个错误

只要块级作用域内存在let命令,这个区域就不再受外部影响

var a = 123
if (true) {
    a = 'abc' // ReferenceError
    let a;
}

使用let声明变量前,该变量都不可用,也就是大家常说的“暂时性死区”

最后,let不允许在相同作用域中重复声明

let a = 20
let a = 30
// Uncaught SyntaxError: Identifier 'a' has already been declared

注意的是相同作用域,下面这种情况是不会报错的

let a = 20
{
    let a = 30
}

因此,我们不能在函数内部重新声明参数

function func(arg) {
  let arg;
}
func()
// Uncaught SyntaxError: Identifier 'arg' has already been declared
三、const

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

const a = 1
a = 3
// TypeError: Assignment to constant variable.

这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值

const a;
// SyntaxError: Missing initializer in const declaration

如果之前用varlet声明过变量,再用const声明同样会报错

var a = 20
let b = 20
const a = 30
const b = 30
// 都会报错

const实际上保证的并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动

对于简单类型的数据,值就保存在变量指向的那个内存地址,因此等同于常量

对于复杂类型的数据,变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的,并不能确保改变量的结构不变

const foo = {};

// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123

// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only

其它情况,constlet一致

四、区别

varletconst三者区别可以围绕下面五点展开:

  • 变量提升
  • 暂时性死区
  • 块级作用域
  • 重复声明
  • 修改声明的变量
  • 使用
变量提升
var `声明的变量存在变量提升,即变量可以在声明之前调用,值为`undefined

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

// var
console.log(a)  // undefined
var a = 10

// let 
console.log(b)  // Cannot access 'b' before initialization
let b = 10

// const
console.log(c)  // Cannot access 'c' before initialization
const c = 10
暂时性死区

var不存在暂时性死区

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

// var
console.log(a)  // undefined
var a = 10

// let
console.log(b)  // Cannot access 'b' before initialization
let b = 10

// const
console.log(c)  // Cannot access 'c' before initialization
const c = 10
块级作用域

var不存在块级作用域

letconst存在块级作用域

// var
{
    var a = 20
}
console.log(a)  // 20

// let
{
    let b = 20
}
console.log(b)  // Uncaught ReferenceError: b is not defined

// const
{
    const c = 20
}
console.log(c)  // Uncaught ReferenceError: c is not defined
重复声明

var允许重复声明变量

letconst在同一作用域不允许重复声明变量

// var
var a = 10
var a = 20 // 20

// let
let b = 10
let b = 20 // Identifier 'b' has already been declared

// const
const c = 10
const c = 20 // Identifier 'c' has already been declared
修改声明的变量

varlet可以

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

// var
var a = 10
a = 20
console.log(a)  // 20

//let
let b = 10
b = 20
console.log(b)  // 20

// const
const c = 10
c = 20
console.log(c) // Uncaught TypeError: Assignment to constant variable
使用

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

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

itpeilibo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值