中小型公司面试(初/中级)时都会问些什么?

CSS盒子模型

盒子模型,顾名思义,可以装东西的称为盒子,比如 div,h,li 等等。像 img,input 这种不能装东西的就不是盒子。 盒模型:分为内容(content)、填充(padding)、边界(margin)、边框(border)四个部分,内容又分为高(height)、宽(width)。 div 高(height)默认为auto,会由子元素的改好、宽(width)

盒子模型类型

IE盒模型(border-box)

IE盒模型:属性 width,height 包含 content、border 和 padding,指的是 content + padding + border 。

W3C标准盒模型(content-box)

W3C 标准盒模型:属性 width ,height 只包含内容 content,不包含 border 和 padding 。

切换盒模型

/* W3C盒子模型 */
box-sizing: content-box;
/* IE盒子模型 */
box-sizing: border-box;

margin负值问题

  • margin-top 元素自身会向上移动,同时会影响下方的元素会向上移动;
  • margin-botom 元素自身不会位移,但是会减少自身供css读取的高度,从而影响下方的元素会向上移动。
  • margin-left 元素自身会向左移动,同时会影响其它元素;
  • margin-right 元素自身不会位移,但是会减少自身供css读取的宽度,从而影响右侧的元素会向左移动;

多种方式实现上面 100px 下面自适应的布局

  • flex 布局
  • gird 布局
  • margin-top + calc
  • 定位 + calc

display 都有哪些属性

描述
none此元素不会被显示。
block此元素将显示为块级元素,此元素前后会带有换行符。inline
table此元素会作为块级表格来显示,表格前后带有换行符。inherit
flex弹性盒模型。
grid网格布局。

块元素和行内元素、行内块元素的区别

块级元素

  • 常见的块元素有哪些?

    • 常见的块元素有<h1>~<h6>、<p>、<div>、<ul>、<ol>、<li>等,其中<div>标签是最典型的块元素。
  • 块级元素有什么特点?

    • 自己独占一行
    • 高度,宽度、外边距以及内边距都可以控制。
    • 宽度默认是容器(父级宽度)的 100%
    • 是一个容器及盒子,里面可以放行内或者块级元素
  • 注意问题:

    • 只有文字才能组成段落,因此 p 标签里面不能放块级元素,特别是 p 标签不能放 div。同理还有这些标签h1,h2,h3,h4,h5,h6,dt ,他们都是文字类块级标签,里面不能放其他块级元素。

行内元素

  • 常见行内元素有哪些?

    • 常见的行内元素有<a>、<strong>、<b>、<em>、<i>、<del>、<s>、<ins>、<u>、<span>等,其中<span>标签最典型的行内元素,也称内联元素。
  • 行内元素有哪些特点?

    • 相邻行内元素在一行上,一行可以显示多个
    • 高、宽直接设置是无效的
    • 只可以设置水平方向的外边距
    • 默认宽度就是它本身内容的宽度
    • 行内元素只能容纳文本或则其他行内元素
  • 注意问题:

    • 链接里面不能再放链接。
    • 特殊情况 a 里面可以放块级元素,但是给 a 转换一下块级模式最安全。

行内块元素

  • 常见行内块儿元素有哪些?

    • 在行内元素中有几个特殊的标签<img />、<input />、<td>,可以对它们设置宽高和对齐属性,有些资料可能会称它们为行内块元素。
  • 行内块元素有什么特点?

    • 和相邻行内元素(行内块)在一行上,但是之间会有空白缝隙,一行可 - 以显示多个。
    • 默认宽度就是它本身内容的宽度。
    • 高度,行高、外边距以及内边距都可以控制。

块级元素、行内元素和行内块元素的区别

元素模式元素排列设置样式默认宽度包含
块级元素一行只能放一个块级元素可以设置宽度高度容器的100%容器级可以包含任何标签
行内元素一行可以放多个行内元素不可以直接设置宽度高度它本身内容的宽度容纳文本或则其他行内元素
行内块元素一行放多个行内块元素可以设置宽度和高度它本身内容的宽度

块级元素、行内元素和行内块元素互转

  • 块转行内:display:inline;
  • 行内转块:display:block;
  • 块、行内元素转换为行内块:display: inline-block;

原型和原型链

引用类型皆为对象

原型和原型链都是来源于对象而服务于对象的概念,所以我们要先明确一点:

JavaScript中一切引用类型都是对象,对象就是属性的集合。 Array类型、Function类型、Object类型、Date类型、RegExp类型等都是引用类型。

也就是说 数组是对象、函数是对象、正则是对象、对象还是对象。

原型和原型链是什么

上面我们说到对象就是属性(property)的集合,有人可能要问不是还有方法吗?其实方法也是一种属性,因为它也是键值对的表现形式

obj.sayHello = function() {alert('hello')}

obj.hasOwnProperty('sayHello') // true

可以看到obj上确实多了一个sayHello的属性,值为一个函数,但是问题来了,obj上面并没有hasOwnProperty这个方法,为什么我们可以调用呢?这就引出了 原型。

每一个对象从被创建开始就和另一个对象关联,从另一个对象上继承其属性,这个另一个对象就是 原型。

当访问一个对象的属性时,先在对象的本身找,找不到就去对象的原型上找,如果还是找不到,就去对象的原型(原型也是对象,也有它自己的原型)的原型上找,如此继续,直到找到为止,或者查找到最顶层的原型对象中也没有找到,就结束查找,返回undefined。

这条由对象及其原型组成的链就叫做原型链。

现在我们已经初步理解了原型和原型链,到现在大家明白为什么数组都可以使用push、slice等方法,函数可以使用call、bind等方法了吧,因为在它们的原型链上找到了对应的方法。

  • 总结一下:
    • 原型存在的意义就是组成原型链:引用类型皆对象,每个对象都有原型,原型也是对象,也有它自己的原型,一层一层,组成原型链。
    • 原型链存在的意义就是继承:访问对象属性时,在对象本身找不到,就在原型链上一层一层找。说白了就是一个对象可以访问其他对象的属性。
    • 继承存在的意义就是属性共享:好处有二:一是代码重用,字面意思;二是可扩展,不同对象可能继承相同的属性,也可以定义只属于自己的属性。

创建对象

对象的创建方式主要有两种,一种是new操作符后跟函数调用,另一种是字面量表示法。

目前我们现在可以理解为:所有对象都是由new操作符后跟函数调用来创建的,字面量表示法只是语法糖(即本质也是new,功能不变,使用更简洁)。

// new操作符后跟函数调用
let obj = new Object()
let arr = new Array()

// 字面量表示法
let obj = { a: 1}
// 等同于
let obj = new Object()
obj.a = 1

let arr = [1,2]
// 等同于
let arr = new Array()
arr[0] = 1
arr[1] = 2

Object、Array等称为构造函数,不要怕这个概念,构造函数和普通函数并没有什么不同,只是由于这些函数常被用来跟在new后面创建对象。new后面调用一个空函数也会返回一个对象,任何一个函数都可以当做构造函数。

所以构造函数更合理的理解应该是函数的构造调用。

Number、String、Boolean、Array、Object、Function、Date、RegExp、Error这些都是函数,而且是原生构造函数,在运行时会自动出现在执行环境中。

构造函数是为了创建特定类型的对象,这些通过同一构造函数创建的对象有相同原型,共享某些方法。举个例子,所有的数组都可以调用push方法,因为它们有相同原型。

我们来自己实现一个构造函数:

// 惯例,构造函数应以大写字母开头
function Person(name) {
  // 函数内this指向构造的对象
  // 构造一个name属性
  this.name = name
  // 构造一个sayName方法
  this.sayName = function() {
    console.log(this.name)
  }
}

// 使用自定义构造函数Person创建对象
let person = new Person('logan')
person.sayName() // 输出:logan

总结一下:构造函数用来创建对象,同一构造函数创建的对象,其原型相同。

__proto__与prototype

万物逃不开真香定律,初步了解了相关知识,我们也要试着来理解一下这些头疼的单词,并且看一下指来指去的箭头了。

上面总结过,每个对象都有原型,那么我们怎么获取到一个对象的原型呢?那就是对象的__proto__属性,指向对象的原型。

上面也总结过,引用类型皆对象,所以引用类型都有__proto__属性,对象有__proto__属性,函数有__proto__属性,数组也有__proto__属性,只要是引用类型,就有__proto__属性,都指向它们各自的原型对象。

__proto__属性虽然在ECMAScript 6语言规范中标准化,但是不推荐被使用,现在更推荐使用Object.getPrototypeOf,Object.getPrototypeOf(obj)也可以获取到obj对象的原型。本文中使用__proto__只是为了便于理解。

Object.getPrototypeOf(person) === person.__proto__ // true

上面说过,构造函数是为了创建特定类型的对象,那如果我想让Person这个构造函数创建的对象都共享一个方法,总不能像下面这样吧:

错误示范

// 调用构造函数Person创建一个新对象personA
let personA = new Person('张三')
// 在personA的原型上添加一个方法,以供之后Person创建的对象所共享
personA.__proto__.eat = function() {
    console.log('吃东西')
}
let personB = new Person('李四')
personB.eat() // 输出:吃东西

但是每次要修改一类对象的原型对象,都去创建一个新的对象实例,然后访问其原型对象并添加or修改属性总觉得多此一举。既然构造函数创建的对象实例的原型对象都是同一个,那么构造函数和其构造出的对象实例的原型对象之间有联系就完美了。

这个联系就是prototype。每个函数拥有prototype属性,指向使用new操作符和该函数创建的对象实例的原型对象。

Person.prototype === person.__proto__ // true

看到这里我们就明白了,如果想让Person创建出的对象实例共享属性,应该这样写:

正确示范

Person.prototype.drink = function() {
    console.log('喝东西')
}

let personA = new Person('张三')
personB.drink() // 输出:喝东西
  • 总结一下:
    • 对象有__proto__属性,函数有__proto__属性,数组也有__proto__属性,只要是引用类型,就有__proto__属性,指向其原型。
    • 只有函数有prototype属性,只有函数有prototype属性,只有函数有prototype属性,指向new操作符加调用该函数创建的对象实例的原型对象。

原型链顶层

原型链之所以叫原型链,而不叫原型环,说明它是有始有终的,那么原型链的顶层是什么呢?

拿我们的person对象来看,它的原型对象,很简单

// 1. person的原型对象
person.__proto__ === Person.prototype

接着往上找,Person.prototype也是一个普通对象,可以理解为Object构造函数创建的,所以得出下面结论,

// 2. Person.prototype的原型对象
Person.prototype.__proto__ === Object.prototype

Object.prototype也是一个对象,那么它的原型呢?这里比较特殊,切记!!!

Object.prototype.__proto__ === null

我们就可以换个方式描述下 原型链 :由对象的__proto__属性串连起来的直到Object.prototype.proto(为null)的链就是原型链。

在上面内容的基础之上,我们来模拟一下js引擎读取对象属性:

function getProperty(obj, propName) {
    // 在对象本身查找
    if (obj.hasOwnProperty(propName)) {
        return obj[propName]
    } else if (obj.__proto__ !== null) {
    // 如果对象有原型,则在原型上递归查找
        return getProperty(obj.__proto__, propName)
    } else {
    // 直到找到Object.prototype,Object.prototype.__proto__为null,返回undefined
        return undefined
    }
}

constructor

回忆一下之前的描述,构造函数都有一个prototype属性,指向使用这个构造函数创建的对象实例的原型对象。

这个原型对象中默认有一个constructor属性,指回该构造函数。

Person.prototype.constructor === Person // true

之所以开头不说,是因为这个属性对我们理解原型及原型链并无太大帮助,反而容易混淆。

函数对象的原型链

之前提到过引用类型皆对象,函数也是对象,那么函数对象的原型链是怎么样的呢?

对象都是被构造函数创建的,函数对象的构造函数就是Function,注意这里F是大写。

let fn = function() {}
// 函数(包括原生构造函数)的原型对象为Function.prototype
fn.__proto__ === Function.prototype // true
Array.__proto__ === Function.prototype // true
Object.__proto__ === Function.prototype // true

Function.prototype也是一个普通对象,所以Function.prototype.proto === Object.prototype

这里有一个特例,Function的__proto__属性指向Function.prototype。

总结一下:函数都是由Function原生构造函数创建的,所以函数的__proto__属性指向Function的prototype属性

  • 知识点
  1. 引用类型都是对象,每个对象都有原型对象。
  2. 对象都是由构造函数创建,对象的__proto__属性指向其原型对象,构造函数的prototype属性指向其创建的对象实例的原型对象,3. 所以对象的__proto__属性等于创建它的构造函数的prototype属性。
  3. 所有通过字面量表示法创建的普通对象的构造函数为Object
  4. 所有原型对象都是普通对象,构造函数为Object所有函数的构造函数是Function
  5. Object.prototype没有原型对象

instanceof操作符

平常我们判断一个变量的类型会使用typeof运算符,但是引用类型并不适用,除了函数对象会返回function外,其他都返回object。我们想要知道一个对象的具体类型,就需要使用到instanceof。

let fn = function() {}
let arr = []
fn instanceof Function // true
arr instanceof Array // true
fn instanceof Object // true
arr instanceof Object // true

为什么fn instanceof Object和arr instanceof Object都返回true呢?我们来看一下MDN上对于instanceof运算符的描述:

instanceof运算符用于测试构造函数的prototype属性是否出现在对象的原型链中的任何位置

也就是说instanceof操作符左边是一个对象,右边是一个构造函数,在左边对象的原型链上查找,知道找到右边构造函数的prototype属性就返回true,或者查找到顶层null(也就是Object.prototype.proto),就返回false。 我们模拟实现一下:

function instanceOf(obj, Constructor) { // obj 表示左边的对象,Constructor表示右边的构造函数
    let rightP = Constructor.prototype // 取构造函数显示原型
    let leftP = obj.__proto__ // 取对象隐式原型
    // 到达原型链顶层还未找到则返回false
    if (leftP === null) {
        return false
    }
    // 对象实例的隐式原型等于构造函数显示原型则返回true
    if (leftP === rightP) {
        return true
    }
    // 查找原型链上一层
    return instanceOf(obj.__proto__, Constructor)
}

现在就可以解释一些比较令人费解的结果了:

fn instanceof Object //true
// 1. fn.__proto__ === Function.prototype
// 2. fn.__proto__.__proto__ === Function.prototype.__proto__ === Object.prototype
arr instanceof Object //true
// 1. arr.__proto__ === Array.prototype
// 2. arr.__proto__.__proto__ === Array.prototype.__proto__ === Object.prototype
Object instanceof Object // true
// 1. Object.__proto__ === Function.prototype
// 2. Object.__proto__.__proto__ === Function.prototype.__proto__ === Object.prototype
Function instanceof Function // true
// Function.__proto__ === Function.prototype

总结一下:instanceof运算符用于检查右边构造函数的prototype属性是否出现在左边对象的原型链中的任何位置。其实它表示的是一种原型链继承的关系。

Object.create

之前说对象的创建方式主要有两种,一种是new操作符后跟函数调用,另一种是字面量表示法。

其实还有第三种就是ES5提供的Object.create()方法,会创建一个新对象,第一个参数接收一个对象,将会作为新创建对象的原型对象,第二个可选参数是属性描述符(不常用,默认是undefined)。具体请查看Object.create()。

我们来模拟一个简易版的Object.create:

function createObj(proto) {
    function F() {}
    F.prototype = proto
    return new F()
}

我们平常所说的空对象,其实并不是严格意义上的空对象,它的原型对象指向Object.prototype,还可以继承hasOwnProperty、toString、valueOf等方法。

如果想要生成一个不继承任何属性的对象,可以使用Object.create(null)。

如果想要生成一个平常字面量方法生成的对象,需要将其原型对象指向Object.prototype:

let obj = Object.create(Object.prototype)
// 等价于
let obj = {}

new操作符

当我们使用new时,做了些什么?

  • 创建一个全新对象,并将其__proto__属性指向构造函数的prototype属性。
  • 将构造函数调用的this指向这个新对象,并执行构造函数。
  • 如果构造函数返回对象类型Object(包含Functoin, Array, Date, RegExg, Error等),则正常返回,否则返回这个新的对象。

依然来模拟实现一下:

function newOperator(func, ...args) {
    if (typeof func !== 'function') {
        console.error('第一个参数必须为函数,您传入的参数为', func)
        return
    }
    // 创建一个全新对象,并将其`__proto__`属性指向构造函数的`prototype`属性
    let newObj = Object.create(func.prototype)
    // 将构造函数调用的this指向这个新对象,并执行构造函数
    let result = func.apply(newObj, args)
    // 如果构造函数返回对象类型Object,则正常返回,否则返回这个新的对象
    return (result instanceof Object) ? result : newObj
}

Function.proto === Function.prototype

其实这里完全没必要去纠结鸡生蛋还是蛋生鸡的问题,我自己的理解是:Function是原生构造函数,自动出现在运行环境中,所以不存在自己生成自己。之所以Function.proto === Function.prototype,是为了表明Function作为一个原生构造函数,本身也是一个函数对象,仅此而已。

真的是继承吗?

前面我们讲到每一个对象都会从原型“继承”属性,实际上,继承是一个十分具有迷惑性的说法,引用《你不知道的JavaScript》中的话,就是:

继承意味着复制操作,然而 JavaScript 默认并不会复制对象的属性,相反,JavaScript 只是在两个对象之间创建一个关联,这样,一个对象就可以通过委托访问另一个对象的属性,所以与其叫继承,委托的说法反而更准确些。

Person.prototype.constructor 是什么

Person.prototype.constructor === Person // true

函数有没有 __ proto __ 属性

let fn = function() {}
// 函数(包括原生构造函数)的原型对象为Function.prototype
fn.__proto__ === Function.prototype // true

函数都是由 Function 原生构造函数创建的,所以函数的 proto 属性指向 Function 的 prototype 属性。

谈一谈 js 数据类型

JavaScript属于什么类型的语言?

  • 使用之前就需要确认其变量类型的称为静态语言
  • 在运行过程中需要检查数据类型的语言称为动态语言
  • 支持隐式类型转换的语言称为弱类型语言,反之为强类型语言
  • JavaScript属于动态语言弱类型语言

JavaScript有多少种数据类型?

  • Number
  • String
  • Boolean
  • undefined
  • null
  • Object
  • Bigint
  • Symbol

我们经常使用的的类型如:array,function 都属于复杂数据类型 Object。

什么是值类型和引用类型?

  • 存储位置

值类型的变量会保存在栈内存中,如果在一个函数中声明一个值类型的变量,那么这个变量当函数执行结束之后会自动销毁

引用类型的变量名会保存在栈内存中,但是变量值会存储在堆内存中,引用类型的变量不会自动销毁,当没有引用变量引用它时,系统的垃圾回收机制会回收它。

  • 赋值方式

值类型的变量直接赋值就是深赋值,修改 b 的值不会影响 a。

function foo() {
    let a = 1
    let b = a
    a = 2
    console.log(a); //=>  2
    console.log(b); //=>  1
}

foo()

引用类型的变量直接赋值实际上是传递引用,只是浅赋值,修改值会影响所有引用该地址的变量。

function foo() {
    let c = {
        name: 'warlber'
    }
    let d = c
    d.name = 'mkimq'
    console.log(c.name); //=>  mkimq
    console.log(d.name); //=>  mkimq
}

foo()
  • 添加属性和方法

值类型无法添加属性和方法。

function foo() {
    let a = 1
    a.name = 'warbler'
    console.log(a.name); //=>  undefined
}
foo()

引用类型可以添加属性和方法。

function foo() {
    let a = {}
    a.name = 'mkimq'
    console.log(a.name); //=>  mkimq
}
foo()

值类型和引用类型分别有哪些?

基本的值类型有: undefined,Boolean,Number,String,Symbol。

引用类型有:Object。

特殊的引用类型:null,指针指向空地址。

特殊的引用类型:function,不用于储存数据,所以没有复制,拷贝函数一说。

什么是虚值和真值?分别有哪些?

简单的来说虚值就是在转换为布尔值时变为false的值,变为true的值则为真值

如何检查值是否虚值?使用Boolean函数或者 **!!**运算符。

  • 虚值(Falsy)

    • 长度为 0 的字符串
    • 数字 0
    • false
    • undefined
    • null
    • NaN
  • 真值(Truthy)

    • 空数组
    • 空对象
    • 其他

&& 和 || 运算符能做什么?

&& 也可以叫 逻辑与, 它采用短路来防止不必要的工作 。

  • 在其操作数中找到第一个虚值表达式并返回它,如果没有找到任何虚值表达式,则返回最后一个真值表达式。
  • 所有操作数的条件都为 true 时,结果才为 true;
  • 如果有一个为 false,结果就为 false;
  • 当第一个条件为 false 时,就不再判断后面的条件。
console.log(false && 1 && []); // false
console.log(" " && true && 5); // 5

使用if语句

if(value){
    console.log(value)
}

使用&&操作符

value && console.log(value)

|| 也叫或逻辑或,这也使用了短路来防止不必要的工作。

  • 在其操作数中找到第一个真值表达式并返回它;
  • 只要有一个条件为 true 时,结果就为 true;
  • 当两个条件都为 false 时,结果才为 false;
  • 当一个条件为 true 时,后面的条件不再判断;
  • 在支持 ES6 默认函数参数之前,它用于初始化函数中的默认参数值。
console.log(null || 1 || undefined); // 1
function logName(name) {
    var n = name || "Mark";
    console.log(n);
}
logName(); // "Mark"
logName('hzw'); // "hzw"

逻辑与的优先级是高于逻辑或的

比如 console.log(3||2&&5||0),会先算 2 && 5 的值为 5,然后再 3||5----3,最后再 3||0----3,所以最终结果为 3。

表达式a && 表达式b

计算表达式 a(也可以是函数)的运算结果,如果为 True, 执行表达式 b(或函数),并返回 b 的结果; 如果为 False,返回 a 的结果。

表达式a || 表达式b

计算表达式 a(也可以是函数)的运算结果,如果为 Fasle, 执行表达式 b(或函数),并返回 b 的结果;如果为 True,返回 a 的结果。

如何判断数据类型的多种方式,有什么区别,适用场景

ypeof运算符

  • 识别所有值类型;
  • 识别函数类型;
  • 识别引用类型,但是无法区分对象,数组以及 null。
  • Infinity 和 NaN 会被识别为 number,尽管NaN是Not-A-Number的缩写,意思是"不是一个数字"。
  • 我们可以使用 typeof 来检测一个变量是否存在,如 if(typeof a!="undefined"){},而当使用 if(a) 时,如果 a 不存在(未声明),则会报错。
let a
const b = null
const c = 100
const d = 'warbler'
const e = true
const f = Symbol('f')
const foo = () => {}
const arr = []
const obj = {}
console.log(typeof a) //=> undefined
console.log(typeof b) //=> object
console.log(typeof c) //=> number
console.log(typeof d) //=> string
console.log(typeof e) //=> boolean
console.log(typeof f) //=> symbol
console.log(typeof foo) //=> function
console.log(typeof arr) //=> object
console.log(typeof obj) //=> object
console.log(typeof Infinity) //=> number
console.log(typeof NaN) //=> number

instanceof方法

  • 用来检测引用数据类型,值类型都会返回 false。
  • 左操作数是待检测其类的对象,右操作数是对象的类。如果左侧的对象是右侧的实例,则返回 true,否则返回false。
  • 检测所有 new 操作符创建的对象都返回 true。
  • 检测 null 和 undefined 会返回 false。
const foo = () => { }
const arr = []
const obj = {}
const data = new Date()
const number = new Number(3)
console.log(foo instanceof Function) //=> true
console.log(arr instanceof Array) //=> true
console.log(obj instanceof Object) //=> true
console.log(data instanceof Object) //=> true
console.log(number instanceof Object) //=> true
console.log(null instanceof Object) //=> false
console.log(undefined instanceof Object) //=> false

constructor方法

除了 undefined 和 null 之外,其他类型都可以通过 constructor 属性来判断类型。

const c = 100
const d = 'warbler'
const e = true
const f = Symbol('f')
const reg = /^[a-zA-Z]{5,20}$/
const foo = () => { }
const arr = []
const obj = {}
const date = new Date();
const error = new Error();
console.log(c.constructor === Number) //=> true
console.log(d.constructor === String) //=> true
console.log(e.constructor === Boolean) //=> true
console.log(f.constructor === Symbol) //=> true
console.log(reg.constructor === RegExp) //=> true
console.log(foo.constructor === Function) //=> true
console.log(arr.constructor === Array) //=> true
console.log(obj.constructor === Object) //=> true
console.log(date.constructor === Date) //=> true
console.log(error.constructor === Error) //=> true

Object.prototype.toString.call

  • 对于 Object.prototype.toString() 方法,会返回一个形如 [object XXX] 的字符串。
  • 使用Object.prototype.toString.call 的方式来判断一个变量的类型是最准确的方法。
  • Object.prototype.toString.call 换成 Object.prototype.toString.apply 也可以。
let a
const b = null
const c = 100
const d = 'warbler'
const e = true
const f = Symbol('f')
const reg = /^[a-zA-Z]{5,20}$/
const foo = () => { }
const arr = []
const obj = {}
const date = new Date();
const error = new Error();
const args = (function() {
return arguments;
})()
console.log(Object.prototype.toString.call(a)) //=> [object Undefined]
console.log(Object.prototype.toString.call(b)) //=> [object Null]
console.log(Object.prototype.toString.call(c)) //=> [object Number]
console.log(Object.prototype.toString.call(d)) //=> [object String]
console.log(Object.prototype.toString.call(e)) //=> [object Boolean]
console.log(Object.prototype.toString.call(f)) //=> [object Symbol]
console.log(Object.prototype.toString.call(reg)) //=> [object RegExp]
console.log(Object.prototype.toString.call(foo)) //=> [object Function]
console.log(Object.prototype.toString.call(arr)) //=> [object Array]
console.log(Object.prototype.toString.call(obj)) //=> [object Object]
console.log(Object.prototype.toString.call(date)) //=> [object Date]
console.log(Object.prototype.toString.call(error)) //=> [object Error]
console.log(Object.prototype.toString.call(args)) //=> [object Arguments]

封装成简单的函数使用

const getPrototype = (item) => Object.prototype.toString.call(item).split(' ')[1].replace(']', '')
console.log(getPrototype('abc')) //=> String

空值null

我们还可以通过下面的方法来判断变量是否为 null 。

let exp = null;
if (!exp && typeof (exp) !== "undefined" && exp !== 0) {
console.log('exp is null');
}
if (exp === null) {
console.log('exp is null');
}
if (!exp && typeof exp === "object") {
console.log('exp is null');
}

未定义undefined

我们还可以通过下面的种方法来判断变量是否为 undefined 。

let exp;
if (exp === void 0) {
    console.log('exp is undefined');
}

数字

我们还可以通过下面的方法来判断变量是否为 数字 。

let exp = 100;
// isNaN检查不严密 如果 exp 是一个空串或是一个空格,isNaN是会做为数字0进行处理的
if (!isNaN(exp)) {
console.log('exp is number');
}
// 利用正则判断字符串是否为0-9
let reg = /^[0-9]+.?[0-9]*/
if (reg.test(exp)) {
console.log('exp is number');
}
if (parseFloat(exp).toString() !== 'NaN') {
console.log('exp is number');
}

数组

我们还可以通过下面的方法来判断变量是否为 数组 。

let exp = [];
if (Array.isArray(exp)) {
    console.log('exp is Array');
}

Promise 如何一次进行多个异步请求

利用 Promise.all 。

如果想要其中一个请求出错了但是不返回结果怎么办

使用 Promise.allSettled 。

koa 如何启动一个服务器

const Koa = require('koa')
const app = new Koa()

app.use(async ctx => {
    ctx.body = 'hello world'
})

app.listen(3000)

new koa 都做了什么

  • 构建上下文 ctx
  • 构建洋葱圈模型。

长列表优化,一万条数据不用分页和懒加载,如何提升性能

直接渲染

最直接的方式就是直接渲染出来,但是这样的做法肯定是不可取的,因为一次性渲染出10w个节点,是非常耗时间的,咱们可以来看一下耗时,差不多要消耗12秒,非常消耗时间

setTimeout分页渲染

这个方法就是,把10w按照每页数量limit分成总共Math.ceil(total / limit)页,然后利用setTimeout,每次渲染1页数据,这样的话,渲染出首页数据的时间大大缩减了

const renderList = async () => {
    console.time('列表时间')
    const list = await getList()
    console.log(list)
    const total = list.length
    const page = 0
    const limit = 200
    const totalPage = Math.ceil(total / limit)

    const render = (page) => {
        if (page >= totalPage) return
        setTimeout(() => {
            for (let i = page * limit; i < page * limit + limit; i++) {
                const item = list[i]
                const div = document.createElement('div')
                div.className = 'sunshine'
                div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`
                container.appendChild(div)
            }
            render(page + 1)
        }, 0)
    }
    render(page)
    console.timeEnd('列表时间')
}

requestAnimationFrame

使用requestAnimationFrame代替setTimeout,减少了重排的次数,极大提高了性能,建议大家在渲染方面多使用requestAnimationFrame

const renderList = async () => {
    console.time('列表时间')
    const list = await getList()
    console.log(list)
    const total = list.length
    const page = 0
    const limit = 200
    const totalPage = Math.ceil(total / limit)

    const render = (page) => {
        if (page >= totalPage) return
        // 使用requestAnimationFrame代替setTimeout
        requestAnimationFrame(() => {
            for (let i = page * limit; i < page * limit + limit; i++) {
                const item = list[i]
                const div = document.createElement('div')
                div.className = 'sunshine'
                div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`
                container.appendChild(div)
            }
            render(page + 1)
        })
    }
    render(page)
    console.timeEnd('列表时间')
}

文档碎片 + requestAnimationFrame

  • 文档碎片的好处
    • 之前都是每次创建一个div标签就appendChild一次,但是有了文档碎片可以先把1页的div标签先放进文档碎片中,然后一次性appendChild到container中,这样减少了appendChild的次数,极大提高了性能
    • 页面只会渲染文档碎片包裹着的元素,而不会渲染文档碎片
const renderList = async () => {
    console.time('列表时间')
    const list = await getList()
    console.log(list)
    const total = list.length
    const page = 0
    const limit = 200
    const totalPage = Math.ceil(total / limit)

    const render = (page) => {
        if (page >= totalPage) return
        requestAnimationFrame(() => {
            // 创建一个文档碎片
            const fragment = document.createDocumentFragment()
            for (let i = page * limit; i < page * limit + limit; i++) {
                const item = list[i]
                const div = document.createElement('div')
                div.className = 'sunshine'
                div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`
                // 先塞进文档碎片
                fragment.appendChild(div)
            }
            // 一次性appendChild
            container.appendChild(fragment)
            render(page + 1)
        })
    }
    render(page)
    console.timeEnd('列表时间')
}

懒加载

为了比较通俗的讲解,咱们启动一个vue前端项目,后端服务还是开着

其实实现原理很简单,咱们通过一张图来展示,就是在列表尾部放一个空节点blank,然后先渲染第1页数据,向上滚动,等到blank出现在视图中,就说明到底了,这时候再加载第二页,往后以此类推。

至于怎么判断blank出现在视图上,可以使用getBoundingClientRect方法获取top属性

IntersectionObserver 性能更好,但是我这里就拿getBoundingClientRect来举例

<script setup lang="ts">
import { onMounted, ref, computed } from 'vue'
const getList = () => {
    // 跟上面一样的代码
}

const container = ref<HTMLElement>() // container节点
const blank = ref<HTMLElement>() // blank节点
const list = ref<any>([]) // 列表
const page = ref(1) // 当前页数
const limit = 200 // 一页展示
// 最大页数
const maxPage = computed(() => Math.ceil(list.value.length / limit))
// 真实展示的列表
const showList = computed(() => list.value.slice(0, page.value * limit))
const handleScroll = () => {
    // 当前页数与最大页数的比较
    if (page.value > maxPage.value) return
    const clientHeight = container.value?.clientHeight
    const blankTop = blank.value?.getBoundingClientRect().top
    if (clientHeight === blankTop) {
        // blank出现在视图,则当前页数加1
        page.value++
    }
}

onMounted(async () => {
    const res = await getList()
    list.value = res
})
</script>

<template>
  <div id="container" @scroll="handleScroll" ref="container">
    <div class="sunshine" v-for="(item) in showList" :key="item.tid">
      <img :src="item.src" />
      <span>{{ item.text }}</span>
    </div>
    <div ref="blank"></div>
  </div>
</template>

虚拟列表

  • 基本实现
    • 可视区域的高度
    • 列表项的高度
    • 可视区域能展示的列表项个数 = ~~(可视区域高度 / 列表项高度) + 2
    • 开始索引
    • 结束索引
    • 预加载(防止滚动过快,造成暂时白屏)
    • 根据开始索引和结束索引,截取数据展示在可视区域
    • 滚动节流
    • 上下空白区使用padding实现
    • 滑动到底,再次请求数据并拼接
<template>
  <div class="v-scroll" @scroll.passive="doScroll" ref="scrollBox">
    <div :style="blankStyle" style="height: 100%">
      <div v-for="item in tempSanxins" :key="item.id" class="scroll-item">
        <span>{{ item.msg }}</span>
        <img :src="item.src" />
      </div>
    </div>
  </div>
</template>


<script>
import { throttle } from "../../utils/tools";
export default {
  data() {
    return {
      allSanxins: [], // 所有数据
      itemHiehgt: 150, // 列表每一项的宽度
      boxHeight: 0, // 可视区域的高度
      startIndex: 0, // 元素开始索引
    };
  },
  created() {
    // 模拟请求数据
    this.getAllSanxin(30);
  },
  mounted() {
    // 在mounted时获取可视区域的高度
    this.getScrollBoxHeight();
    // 监听屏幕变化以及旋转,都要重新获取可视区域的高度
    window.onresize = this.getScrollBoxHeight;
    window.onorientationchange = this.getScrollBoxHeight;
  },
  methods: {
    getAllSanxin(count) {
        // 模拟获取数据
        const length = this.allSanxins.length;
        for (let i = 0; i < count; i++) {
            this.allSanxins.push({
            id: `sanxin${length + i}`,
            msg: `我是三心${length + i}`,
            // 这里随便选一张图片就行
            src: require("../../src/asset/images/sanxin.jpg").default,
            });
        }
    },
    // 使用节流,提高性能
    doScroll: throttle(function () {
        // 监听可视区域的滚动事件
        // 公式:~~(滚动的距离 / 列表项 ),就能算出已经滚过了多少个列表项,也就能知道现在的startIndex是多少
        // 例如我滚动条滚过了160px,那么index就是1,因为此时第一个列表项已经被滚上去了,可视区域里的第一项的索引是1
        const index = ~~(this.$refs.scrollBox.scrollTop / this.itemHiehgt);
        if (index === this.startIndex) return;
        this.startIndex = index;
        if (this.startIndex + this.itemNum > this.allSanxins.length - 1) {
            this.getAllSanxin(30);
        }
    }, 200),
    getScrollBoxHeight() {
        // 获取可视区域的高度
        this.boxHeight = this.$refs.scrollBox.clientHeight;
    },
  },
  computed: {
    itemNum() {
        // 可视区域可展示多少个列表项? 计算公式:~~(可视化区域高度 / 列表项高度) + 2
        // ~~是向下取整的运算符,等同于Math.floor(),为什么要 +2 ,是因为可能最上面和最下面的元素都只展示一部分
        return ~~(this.boxHeight / this.itemHiehgt) + 2;
    },
    endIndex() {
        // endIndex的计算公式:(开始索引 + 可视区域可展示多少个列表项 * 2)
        // 比如可视区域可展示8个列表项,startIndex是0的话endIndex就是0 + 8 * 2 = 16,startIndex是1的话endIndex就是1 + 8 * 2 = 17,以此类推
        // 为什么要乘2呢,因为这样的话可以预加载出一页的数据,防止滚动过快,出现暂时白屏现象
        let index = this.startIndex + this.itemNum * 2;
        if (!this.allSanxins[index]) {
            // 到底的情况,比如startIndex是99995,那么endIndex本应该是99995 + 8 * 2 = 10011
            // 但是列表数据总数只有10000条,此时就需要让endIndex = (列表数据长度 - 1)
            index = this.allSanxins.length - 1;
        }
        return index;
    },
    tempSanxins() {
        //   可视区域展示的截取数据,使用了数组的slice方法,不改变原数组又能截取
        let startIndex = 0;
        if (this.startIndex <= this.itemNum) {
            startIndex = 0;
        } else {
            startIndex = this.startIndex + this.itemNum;
        }
        return this.allSanxins.slice(startIndex, this.endIndex + 1);
    },
    blankStyle() {
        // 上下方的空白处使用padding来充当
        let startIndex = 0;
        if (this.startIndex <= this.itemNum) {
            startIndex = 0;
        } else {
            startIndex = this.startIndex - this.itemNum;
        }
        return {
            // 上方空白的高度计算公式:(开始index * 列表项高度)
            // 比如你滚过了3个列表项,那么上方空白区高度就是3 * 150 = 450,这样才能假装10000个数据的滚动状态
            paddingTop: startIndex * this.itemHiehgt + "px",
            // 下方空白的高度计算公式:(总数据的个数 - 结束index - 1) * 列表项高度
            // 例如现在结束index是100,那么下方空白高度就是:(10000 - 100 - 1) * 150 = 1,484,850
            paddingBottom:
            (this.allSanxins.length - this.endIndex - 1) * this.itemHiehgt + "px",
            // 不要忘了加px哦
        };
    },
  },
};
</script>

<style lang="scss" scoped>
.v-scroll {
  height: 100%;
  /* padding-bottom: 500px; */
  overflow: auto;

  .scroll-item {
    height: 148px;
    /* width: 100%; */
    border: 1px solid black;
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 0 20px;

    img {
      height: 100%;
    }
  }
}
</style>

let,const,var 有什么区别

块级作用域: 块作用域由{}包括,let 和 const 具有块级作用域,var 不存在块级作用域。块级作用域解决了 ES5 中的两个问题:

  • 内层变量可能覆盖外层变量
  • 用来计数的循环变量泄露为全局变量

变量提升: var 存在变量提升,let 和 const 不存在变量提升,即在变量只能在声明之后使用,否在会报错。

给全局添加属性: 浏览器的全局对象是 window,Node 的全局对象是 global。var 声明的变量为全局变量,并且会将该变量添加为全局对象的属性,但是 let 和 const 不会。

重复声明: var 声明变量时,可以重复声明变量,后声明的同名变量会覆盖之前声明的遍历。const 和 let 不允许重复声明变量。

暂时性死区: 在使用 let、const 命令声明变量之前,该变量都是不可用的。这在语法上,称为暂时性死区。使用 var 声明的变量不存在暂时性死区。

初始值设置: 在变量声明时,var 和 let 可以不用设置初始值。而 const 声明变量必须设置初始值。

指针指向: let 和 const 都是 ES6 新增的用于创建变量的语法。 let 创建的变量是可以更改指针指向(可以重新赋值)。但 const 声明的变量是不允许改变指针的指向。

区别varletconst
是否有块级作用域×
是否存在变量提升××
是否添加全局属性××
能否重复声明变量××
是否存在暂时性死区×
是否必须设置初始值××
能否改变指针指向×

遍历数组的 n 种方法

什么是高阶函数?

高阶函数是对其他函数进行操作的函数,可以将它们作为参数或返回它们。

简单来说,高阶函数是一个函数,它接收函数作为参数或将函数作为输出返回。

函数可以作为参数

function bar(fn){
    if(typeof fn === "function"){
        fn()
    }
}
//调用
bar(function () {})

函数可以作为返回值

function bar(){
    return function (){}
}
//调用
const fn = bar ()
console.log(fn)

JS中的高阶函数

map

  • map()返回一个新的数组,数组中的元素为原始数组调用函数处理后的值。
  • map()不会对空数组进行检测。
  • map()不会改变原始数组。

传递给 map() 方法的回调函数接受 3 个参数:currentValue,index 和 array。

  • currentValue:必须。当前元素的的值。
  • index:可选。当前元素的索引。
  • arr:可选。当前元素属于的数组对象。

filter

  • filter()方法创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素。
  • filter()不会对空数组进行检测。
  • filter()不会改变原始数组。

传递给 filter() 方法的回调函数接受 3 个参数:currentValue,index 和 array。

  • currentValue:必须。当前元素的的值。
  • index:可选。当前元素的索引。
  • arr:可选。当前元素属于的数组对象。

forEach

  • forEach()方法类似于 map(),传入的函数不需要返回值,并将元素传递给回调函数。
  • forEach() 对于空数组是不会执行回调函数的。
  • forEach()不会返回新的数组,总是返回undefined.

传递给 forEach() 方法的回调函数接受 3 个参数:currentValue,index 和 array。

  • currentValue:必须。当前元素的的值。
  • index:可选。当前元素的索引。
  • arr:可选。当前元素属于的数组对象。

sort

  • sort()方法用于对数组的元素进行排序。
  • sort()会修改原数组。

sort() 方法接受一个可选参数,用来规定排序顺序,必须是函数。

如果没有传递参数, sort() 方法默认把所有元素先转换为 String 再排序 ,根据 ASCII 码进行排序。

如果想按照其他标准进行排序,就需要提供比较函数,该函数要比较两个值,然后返回一个用于说明这两个值的相对顺序的数字。比较函数应该具有两个参数 a 和 b,其返回值如下:

  • 若 a 小于 b,在排序后的数组中 a 应该出现在 b 之前,则返回一个小于 0 的值。
  • 若 a 等于 b,则返回 0。
  • 若 a 大于 b,则返回一个大于 0 的值。

some

  • some() 方法用于检测数组中的元素是否满足指定条件。
  • some() 方法会依次执行数组的每个元素。
  • 如果有一个元素满足条件,则表达式返回 true, 剩余的元素不会再执行检测。
  • 如果没有满足条件的元素,则返回 false 。
  • some()不会对空数组进行检测。
  • some()不会改变原始数组。

传递给 some() 方法的回调函数接受 3 个参数:currentValue,index 和 array。

  • currentValue:必须。当前元素的的值。
  • index:可选。当前元素的索引。
  • arr:可选。当前元素属于的数组对象。

every

  • every() 方法用于检测数组所有元素是否都符合指定条件。
  • every() 方法会依次执行数组的每个元素。
  • 如果数组中检测到有一个元素不满足,则整个表达式返回 false ,且剩余的元素不会再进行检测。
  • 如果所有元素都满足条件,则返回 true。
  • every()不会对空数组进行检测。
  • every()不会改变原始数组。

传递给 every() 方法的回调函数接受 3 个参数:currentValue,index 和 array。

  • currentValue:必须。当前元素的的值。
  • index:可选。当前元素的索引。
  • arr:可选。当前元素属于的数组对象。

reduce

  • reduce() 方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值。
  • reduce()对于空数组是不会执行回调函数的。

reduce 方法接收两个参数

  • 回调函数
  • 一个可选的 initialValue (初始值)。如果不传第二个参数 initialValue,则函数的第一次执行会将数组中的第一个元素作为 prev 参数返回。

传递给 reduce() 方法的回调函数接受 4 个参数:prev, current, currentIndex, arr。

  • prev:必须。函数传进来的初始值或上一次回调的返回值。
  • current:必须。数组中当前处理的元素值。
  • currentIndex:可选。当前元素索引。
  • arr:可选。当前元素所属的数组本身。

reduceRight

reduceRight() 方法的功能和 reduce() 功能是一样的,不同的是 reduceRight() 从数组的末尾向前将数组中的数组项做累加。

find

  • find()方法用于查找符合条件的第一个元素,如果找到了,返回这个元素,否则,返回undefined。
  • find()不会对空数组进行检测。
  • find()不会改变原始数组。

传递给 find() 方法的回调函数接受 3 个参数:currentValue,index 和 array。

  • currentValue:必须。当前元素的的值。
  • index:可选。当前元素的索引。
  • arr:可选。当前元素属于的数组对象。

findIndex

findindex() 和 find() 类似,也是查找符合条件的第一个元素,不同之处在于 findindex() 会返回这个元素的索引,如果没有找到,返回 -1 。

说一下 vue 有哪些优点和特点

  • 渐进式框架:可以在任何项目中轻易的引入;
  • 轻量级框架:只关注视图层,是一个构建数据的视图集合,大小只有几十 kb ;
  • 简单易学:国人开发,中文文档,不存在语言障碍 ,易于理解和学习;
  • 双向数据绑定:在数据操作方面更为简单;
  • 组件化:很大程度上实现了逻辑的封装和重用,在构建单页面应用方面有着独特的优势;
  • 视图数据结构分离:使数据的更改更为简单,不需要进行逻辑代码的修改,只需要操作数据就能完成相关操作;

什么是虚拟 dom

Virtual DOM 是 DOM 节点在 JavaScript 中的一种抽象数据结构,之所以需要虚拟 DOM,是因为浏览器中操作 DOM 的代价比较昂贵,频繁操作 DOM 会产生性能问题。

虚拟 DOM 的作用是在每一次响应式数据发生变化引起页面重渲染时,Vue 对比更新前后的虚拟 DOM,匹配找出尽可能少的需要更新的真实 DOM,从而达到提升性能的目的。

虚拟 DOM 的实现原理主要包括以下 3 部分:

  • 用 JavaScript 对象模拟真实 DOM 树,对真实 DOM 进行抽象;
  • diff 算法 — 比较两棵虚拟 DOM 树的差异;
  • pach 算法 — 将两个虚拟 DOM 对象的差异应用到真正的 DOM 树。

vue 组件间传值的 n 种方式

  • props / $emit 适用 父子组件通信

  • ref 适用 父子组件通信

    • ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例
  • $parent / $children / $root:访问父 / 子实例 / 根实例

  • EventBus ($emit / $on) 适用于 父子、隔代、兄弟组件通信

这种方法通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件。

  • $attrs/$listeners 适用于 隔代组件通信

    • $attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 ( class 和 style 除外 )。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 ( class 和 style 除外 ),并且可以通过 v-bind="$attrs" 传入内部组件。通常配合 inheritAttrs 选项一起使用。
    • $listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件
  • provide / inject 适用于 隔代组件通信

祖先组件中通过 provide 来提供变量,然后在子孙组件中通过 inject 来注入变量。provide / inject API 主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。

  • Vuex 适用于 父子、隔代、兄弟组件通信

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。store 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。

  • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
  • 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。

vue3可以用pinia

  • 插槽

Vue3 可以通过 usesolt 获取插槽数据。

  • mitt.js 适用于任意组件通信

Vue3 中移除了 $on,$off等方法,所以 EventBus 不再使用,相应的替换方案就是 mitt.js

vue有哪些内置指令open in new window

v-show 和 v-if 有什么区别

  • 手段:v-if 是动态的向 DOM 树内添加或者删除 DOM 元素;v-show 是通过设置 DOM 元素的 display 样式属性控制显隐;
  • 编译过程:v-if 切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show 只是简单的基于 css 切换;
  • 编译条件:v-if 是惰性的,如果初始条件为假,则什么也不做;只有在条件第一次变为真时才开始局部编译; v-show 是在任何条件下,无论首次条件是否为真,都被编译,然后被缓存,而且 DOM 元素保留;
  • 性能消耗:v-if 有更高的切换消耗;v-show 有更高的初始渲染消耗;
  • 使用场景:v-if 适合运营条件不大可能改变;v-show 适合频繁切换。

如何让 CSS 只在当前组件中起作用

在组件中的 style 标签中加上 scoped

如何解决 vue 初始化页面闪动问题

使用 vue 开发时,在 vue 初始化之前,由于 div 是不归 vue 管的,所以我们写的代码在还没有解析的情况下会容易出现花屏现象,看到类似于{{message}}的字样,虽然一般情况下这个时间很短暂,但是我们还是有必要让解决这个问题的。

首先:在 css 里加上 [v-cloak] { display: none; } 。如果没有彻底解决问题,则在根元素加上style="display: none;" :style="{display:  block }"

什么是 SPA,有什么优点和缺点

SPA 仅在 Web 页面初始化时加载相应的 HTML、JavaScript 和 CSS。一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载。

  • 优点:

    • 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;
    • 有利于前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理;
  • 缺点:

    • 初次加载耗时多:为实现单页 Web 应用功能及显示效果,需要在加载页面的时候将 JavaScript、CSS 统一加载,部分页面按需加载;
    • 不利于 SEO:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势。

vue 首屏渲染优化有哪些

  • 图片压缩/懒加载
  • 禁止生成 .map 文件
  • 路由懒加载
  • cdn 引入公共库
  • 开启 GZIP 压缩

vue 生命周期函数有哪些open in new window

第一次页面加载会触发哪几个钩子

beforeCreate,created,beforeMount,mounted

在哪个生命周期中发起数据请求

可以在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值 。 推荐在 created 钩子函数中调用异步请求,有以下优点:

  • 能更快获取到服务端数据,减少页面 loading 时间;
  • ssr 不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于一致性;

vue-router 有几种模式

vue-router 有 3 种路由模式:hash、history、abstract:

  • hash: 使用 URL hash 值来作路由。支持所有浏览器,包括不支持 HTML5 History Api 的浏览器;
  • history : 依赖 HTML5 History API 和服务器配置。
  • abstract : 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式.

hash 和 history 有什么区别

hash 模式的实现原理

早期的前端路由的实现就是基于 location.hash 来实现的。其实现原理很简单,location.hash 的值就是 URL 中 # 后面的内容。比如下面这个网站,它的 location.hash 的值为 #search:

https://www.word.com#search
  • hash 路由模式的实现主要是基于下面几个特性:
    • URL 中 hash 值只是客户端的一种状态,也就是说当向服务器端发出请求时,hash 部分不会被发送;
    • hash 值的改变,都会在浏览器的访问历史中增加一个记录。因此我们能通过浏览器的回退、前进按钮控制 hash 的切换;
    • 可以通过 a 标签,并设置 href 属性,当用户点击这个标签后,URL 的 hash 值会发生改变;或者使用 JavaScript 来对 loaction.hash 进行赋值,改变 URL 的 hash 值;
    • 我们可以使用 hashchange 事件来监听 hash 值的变化,从而对页面进行跳转(渲染)。

history 模式的实现原理

HTML5 提供了 History API 来实现 URL 的变化。其中做最主要的 API 有以下两个:history.pushState() 和 history.repalceState()。这两个 API 可以在不进行刷新的情况下,操作浏览器的历史纪录。唯一不同的是,前者是新增一个历史记录,后者是直接替换当前的历史记录,如下所示:

window.history.pushState(null, null, path);
window.history.replaceState(null, null, path);
  • history 路由模式的实现主要基于存在下面几个特性:
    • pushState 和 repalceState 两个 API 来操作实现 URL 的变化 ;
    • 我们可以使用 popstate 事件来监听 url 的变化,从而对页面进行跳转(渲染);
    • history.pushState() 或 history.replaceState() 不会触发 popstate 事件,这时我们需要手动触发页面跳转(渲染)。

vuex 有哪些属性

  • State:定义了应用状态的数据结构,可以在这里设置默认的初始状态。
  • Getter:允许组件从 Store 中获取数据,mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性。
  • Mutation:是唯一更改 store 中状态的方法,且必须是同步函数。
  • Action:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作。
  • Module:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中。

ajax 是什么?有什么优缺点

ajax 是一种创建交互网页应用的一门技术。

  • 优点:

    • 实现局部更新(无刷新状态下),
    • 减轻了服务器端的压力
  • 缺点:

    • 破坏了浏览器前进和后退机制(因为 ajax 自动更新机制)
    • ajax 请求多了,也会出现页面加载慢的情况。
    • 搜索引擎的支持程度比较低。
    • ajax 的安全性问题不太好(可以用数据加密解决)。

同步和异步的区别

  • 同步:同步的思想是:所有的操作都做完,才返回给用户。这样用户在线等待的时间太长,给用户一种卡死了的感觉(就是系统迁移中,点击了迁移,界面就不动了,但是程序还在执行,卡死了的感觉)。这种情况下,用户不能关闭界面,如果关闭了,即迁移程序就中断了。

  • 异步:将用户请求放入消息队列,并反馈给用户,系统迁移程序已经启动,你可以关闭浏览器了。然后程序再慢慢地去写入数据库去。这就是异步。但是用户没有卡死的感觉,会告诉你,你的请求系统已经响应了。你可以关闭界面了。

贡献者: mankueng