[TOC]

JS 基础知识点

1,介绍js的基本数据类型(原始数据类型)。

Undefined、Null、Boolean、Number、String、Symbol(es6新增)

2,类型判断用到哪些方法?

typeof
typeof xxx得到的值有以下几种类型:undefined boolean number string object function、symbol ,比较简单,不再一一演示了。这里需要注意的有三点:

  • typeof null结果是object ,实际这是typeof的一个bug,null是原始值,非引用类型
  • typeof [1, 2]结果是object,结果中没有array这一项,引用类型除了function其他的全部都是object
  • typeof Symbol() 用typeof获取symbol类型的值得到的是symbol,这是 ES6 新增的知识

instanceof
用于实例和构造函数的对应。例如判断一个变量是否是数组,使用typeof无法判断,但可以使用[1, 2] instanceof Array来判断。因为,[1, 2]是数组,它的构造函数就是Array。同理:
自动检测
function Foo(name) {
this.name = name
}
var foo = new Foo(‘bar’)
console.log(foo instanceof Foo) // true

3,JavaScript有几种类型的值?,你能画一下他们的内存图吗?

javascript - 图1
栈:原始数据类型(Undefined,Null,Boolean,Number、String)
堆:引用数据类型(对象、数组和函数)

两种类型的区别是:存储位置不同;
1,原始数据类型直接存储在栈(stack)中的简单数据段,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储;
2,引用数据类型存储在堆(heap)中的对象,占据空间大、大小不固定,如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体

在参数传递方式上,原始类型是按值传递,引用类型是按共享传递

JS 中这种设计的原因是:按值传递的类型,复制一份存入栈内存,这类类型一般不占用太多内存,而且按值传递保证了其访问速度。按共享传递的类型,是复制其引用,而不是整个复制其值(C 语言中的指针),保证过大的对象等不会因为不停复制内容而造成内存的浪费。

4,值类型 vs 引用类型

除了原始类型,ES 还有引用类型,上文提到的typeof识别出来的类型中,只有object和function是引用类型,其他都是值类型。
根据 JavaScript 中的变量类型传递方式,又分为值类型引用类型,值类型变量包括 Boolean、String、Number、Undefined、Null,引用类型包括了 Object 类的所有,如 Date、Array、Function 等。在参数传递方式上,值类型是按值传递,引用类型是按共享传递。
下面通过一个小题目,来看下两者的主要区别,以及实际开发中需要注意的地方。
自动检测
// 值类型
var a = 10
var b = a
b = 20
console.log(a) // 10
console.log(b) // 20
上述代码中,a b都是值类型,两者分别修改赋值,相互之间没有任何影响。再看引用类型的例子:
自动检测
// 引用类型
var a = {x: 10, y: 20}
var b = a
b.x = 100
b.y = 200
console.log(a) // {x: 100, y: 200}
console.log(b) // {x: 100, y: 200}

上述代码中,a b都是引用类型。在执行了b = a之后,修改b的属性值,a的也跟着变化。因为a和b都是引用类型,指向了同一个内存地址,即两者引用的是同一个值,因此b修改属性时,a的值随之改动。
再借助题目进一步讲解一下。

说出下面代码的执行结果,并分析其原因。

自动检测
function foo(a){
a = a * 10;
}
function bar(b){
b.value = ‘new’;
}
var a = 1;
var b = {value: ‘old’};
foo(a);
bar(b);
console.log(a); // 1
console.log(b); // value: new

通过代码执行,会发现:

  • a的值没有发生改变
  • 而b的值发生了改变

这就是因为Number类型的a是按值传递的,而Object类型的b是按共享传递的。
JS 中这种设计的原因是:按值传递的类型,复制一份存入栈内存,这类类型一般不占用太多内存,而且按值传递保证了其访问速度。按共享传递的类型,是复制其引用,而不是整个复制其值(C 语言中的指针),保证过大的对象等不会因为不停复制内容而造成内存的浪费。

引用类型经常会在代码中按照下面的写法使用,或者说容易不知不觉中造成错误
自动检测
var obj = {
a: 1,
b: [1,2,3]
}
var a = obj.a
var b = obj.b
a = 2
b.push(4)
console.log(obj, a, b)

虽然obj本身是个引用类型的变量(对象),但是内部的a和b一个是值类型一个是引用类型,a的赋值不会改变obj.a,但是b的操作却会反映到obj对象上。

5,介绍js有哪些内置对象?

Object 是 JavaScript 中所有对象的父对象
数据封装类对象:Object、Array、Boolean、Number 和 String
其他对象:Function、Arguments、Math、Date、RegEx、Error

6,如何区分数组和对象?

1、从原型入手,Array.prototype.isPrototypeOf(obj); 利用isPrototypeOf()方法,判定Array是不是在obj的原型链中,如果是,则返回true,否则false。Array.prototype.isPrototype([]) //true
2、也可以从构造函数入手,利用对向的constructor属性
3、根据对象的class属性(类属性),跨原型链调用toString()方法。Object.prototype.toString.call(Window);
4、Array.isArray()方法。

7,null,undefined 的区别?

null 表示一个对象被定义了,值为“空值”;
undefined 表示不存在这个值。

typeof undefined //“undefined”
undefined :是一个表示”无”的原始值或者说表示”缺少值”,就是此处应该有一个值,但是还没有定义。当尝试读取时会返回 undefined;
例如变量被声明了,但没有赋值时,就等于undefined

typeof null //“object”
null : 是一个对象(空对象, 没有任何属性和方法);
例如作为函数的参数,表示该函数的参数不是对象;

注意:
在验证null时,一定要使用 === ,因为 == 无法分别 null 和 undefined
undefined表示”缺少值”,就是此处应该有一个值,但是还没有定义。典型用法是:
1)变量被声明了,但没有赋值时,就等于undefined。
2) 调用函数时,应该提供的参数没有提供,该参数等于undefined。
3)对象没有赋值的属性,该属性的值为undefined。
4)函数没有返回值时,默认返回undefined。

null表示”没有对象”,即该处不应该有值。典型用法是:
1) 作为函数的参数,表示该函数的参数不是对象。
2) 作为对象原型链的终点。

8,数组和对象有哪些原生方法,列举一下?

数组:
改变原数组的方法:
不改变原数组的方法:
数组便利方法:forEach, map
转换方法:
对象:

9,字符串有哪些原生方法,列举一下?

定位:
charAt() 返回在指定位置的字符;
charCodeAt() 返回在指定位置的字符的Unicode编码;

indexOf() 返回某个制动的字符值在字符串中首次出现的位置;
lastIndexOf() 从后向前搜索字符串;

合并/截取:
concat() 合并字符串
slice(头,尾) 提取字符串片段 (不包含结束)
substr(头,个数) 含头含尾
substring(头,尾) 含头不含尾

slice、substr、substring三者的用途是一样的,只是截取开闭区间的问题

match() 查找匹配的字符串;
replace() 查找到匹配的字符串,并替换掉;
字符串和数组的相互转换方法:
split() 把字符串转换成数组;
join() 把数组转换成字符串;

10,声明变量和声明函数的提升有什么区别?

(1) 变量声明提升:变量声明在进入执行上下文就完成了。
只要变量在代码中进行了声明,无论它在哪个位置上进行声明, js引擎都会将它的声明放在范围作用域的顶部;

(2) 函数声明提升:执行代码之前会先读取函数声明,意味着可以把函数申明放在调用它的语句后面。
只要函数在代码中进行了声明,无论它在哪个位置上进行声明, js引擎都会将它的声明放在范围作用域的顶部;

(3) 变量or函数声明:函数声明会覆盖变量声明,但不会覆盖变量赋值。
同一个名称标识a,即有变量声明var a,又有函数声明function a() {},不管二者声明的顺序,函数声明会覆盖变量声明,也就是说,此时a的值是声明的函数function a() {}。注意:如果在变量声明的同时初始化a,或是之后对a进行赋值,此时a的值变量的值。eg: var a; var c = 1; a = 1; function a() { return true; } console.log(a);

11,== 和 === 的区别

对于 == 来说,如果对比双方的类型不一样的话,就会进行类型转换

假如我们需要对比 x 和 y 是否相同,就会进行如下判断流程:

首先会判断两者类型是否相同。相同的话就是比大小了
类型不相同的话,那么就会进行类型转换
会先判断是否在对比 null 和 undefined,是的话就会返回 true
判断两者类型是否为 string 和 number,是的话就会将字符串转换为 number
1 == ‘1’

1 == 1
判断其中一方是否为 boolean,是的话就会把 boolean 转为 number 再进行判断
‘1’ == true

‘1’ == 1

1 == 1
判断其中一方是否为 object 且另一方为 string、number 或者 symbol,是的话就会把 object 转为原始类型再进行判断
‘1’ == { name: ‘yck’ }

‘1’ == ‘[object Object]’

javascript - 图2

12,谈谈变量提升

当执行 JS 代码时,会生成执行环境,只要代码不是写在函数中的,就是在全局执行环境中,函数中的代码会产生函数执行环境,只此两种执行环境。

接下来让我们看一个老生常谈的例子,var
自动检测
b() // call b
console.log(a) // undefined
var a = ‘Hello world’
function b() {
console.log(‘call b’)
}

想必以上的输出大家肯定都已经明白了,这是因为函数和变量提升的原因。通常提升的解释是说将声明的代码移动到了顶部,这其实没有什么错误,便于大家理解。但是更准确的解释应该是:在生成执行环境时,会有两个阶段。第一个阶段是创建的阶段,JS 解释器会找出需要提升的变量和函数,并且给他们提前在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明并且赋值为 undefined,所以在第二个阶段,也就是代码执行阶段,我们可以直接提前使用。

在提升的过程中,相同的函数会覆盖上一个函数,并且函数优先于变量提升
自动检测
b() // call b second
function b() {
console.log(‘call b fist’)
}
function b() {
console.log(‘call b second’)
}
var b = ‘Hello world’
var 会产生很多错误,所以在 ES6中引入了 let。let 不能在声明前使用,但是这并不是常说的 let 不会提升,let 提升了,在第一阶段内存也已经为他开辟好了空间,但是因为这个声明的特性导致了并不能在声明前使用。

13,break和continue的区别

break语句可以用于循环语句,也可以用于switch(分子语句),而continue只能用在循环语句中
braek用于终止循环,continue用来跳出本次循环

原型,原型链

1,JavaScript原型,原型链 ? 有什么特点?

原型
每个对象都会在其内部初始化一个属性,就是prototype(proto原型)
使用hasOwnProperty() 可以判断这个属性是不是对象本身的属性

问题:Javascript中,有一个函数,执行时对象查找时,永远不会去查找原型,这个函数是?
hasOwnProperty
javaScript中hasOwnProperty函数方法是返回一个布尔值,指出一个对象是否具有指定名称的属性。此方法无法检查该对象的原型链中是否具有该属性;该属性必须是对象本身的一个成0员。

使用方法:
object.hasOwnProperty(proName)
其中参数object是必选项。一个对象的实例。
proName是必选项。一个属性名称的字符串值。

如果 object 具有指定名称的属性,那么JavaScript中hasOwnProperty函数方法返回 true,反之则返回 false。

问题:如何理解 JavaScript 的原型
对于这个问题,可以从下面这
几个要点来理解和回答,下面几条必须记住并且理解

  • 所有的引用类型(数组、对象、函数),都具有对象特性,即可自由扩展属性(null除外)
  • 所有的引用类型(数组、对象、函数),都有一个proto属性,属性值是一个普通的对象
  • 所有的函数,都有一个prototype属性,属性值也是一个普通的对象
  • 所有的引用类型(数组、对象、函数),proto属性值指向它的构造函数的prototype属性值

通过代码解释一下,大家可自行运行以下代码,看结果。
自动检测
// 要点一:自由扩展属性
var obj = {}; obj.a = 100;
var arr = []; arr.a = 100;
function fn () {}
fn.a = 100;
// 要点二:proto
console.log(obj.proto);
console.log(arr.proto);
console.log(fn.proto);
// 要点三:函数有 prototype
console.log(fn.prototype)
// 要点四:引用类型的 proto 属性值指向它的构造函数的 prototype 属性值
console.log(obj.proto === Object.prototype)
原型
先写一个简单的代码示例。
自动检测
// 构造函数
function Foo(name, age) {
this.name = name
}
Foo.prototype.alertName = function () {
alert(this.name)
}
// 创建示例
var f = new Foo(‘zhangsan’)
f.printName = function () {
console.log(this.name)
}
// 测试
f.printName() // undefined
f.alertName() // zhangsan

执行printName时很好理解,但是执行alertName时发生了什么?这里再记住一个重点 当试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么会去它的proto(即它的构造函数的prototype)中寻找,因此f.alertName就会找到Foo.prototype.alertName。
那么如何判断这个属性是不是对象本身的属性呢?使用hasOwnProperty,常用的地方是遍历一个对象的时候。
自动检测
var item
for (item in f) {
// 高级浏览器已经在 for in 中屏蔽了来自原型的属性,但是这里建议大家还是加上这个判断,保证程序的健壮性
if (f.hasOwnProperty(item)) {
console.log(item)
}
}

原型链
当我们在访问一个对象的属性时,如果这个对象内部不存在这个属性,那么他就会去prototype里找这个属性,这个prototype又会有自己的prototype,于是就这样一直找下去,找到Object.proto为止,找不到就返回undefined也就是我们平时所说的原型链的概念。
关系:instance.constructor.prototype = instance.proto
特点:
JavaScript对象是通过引用来传递的,我们创建的每个新对象实体中并没有一份属于自己的原型副本。当我们修改原型时,与之相关的对象也会继承这一改变。
当我们需要一个属性的时,Javascript引擎会先看当前对象中是否有这个属性, 如果没有的话,就会查找他的Prototype对象是否有这个属性,如此 递推下去,一直检索到 Object 内建对象。 最上层是什么 —— Object.prototype.proto === null

所有的引用类型(数组、对象、函数),都具有对象特性,即可自由扩展属性(null除外)
所有的引用类型(数组、对象、函数),都有一个proto属性,属性值是一个普通的对象
所有的函数,都有一个prototype属性,属性值也是一个普通的对象
所有的引用类型(数组、对象、函数),proto属性值指向它的构造函数的prototype属性值

原型链中的this
所有从原型或更高级原型中得到、执行的方法,其中的this在执行时,就指向了当前这个触发事件执行的对象。

2,谈谈This对象的理解

先搞明白一个很重要的概念 —— this的值是在执行的时候才能确认,定义的时候不能确认! 为什么呢 —— 因为this是执行上下文环境的一部分,而执行上下文需要在代码执行之前确定,而不是定义的时候。看如下例子

自动检测
var a = {
name: ‘A’,
fn: function () {
console.log(this.name)
}
}
a.fn() // this === a
a.fn.call({name: ‘B’}) // this === {name: ‘B’}
var fn1 = a.fn
fn1() // this === window

this执行会有不同,主要集中在这几个场景中

  • 作为构造函数执行,构造函数中
  • 作为对象属性执行,上述代码中a.fn()
  • 作为普通函数执行,上述代码中fn1()
  • 用于call apply bind,上述代码中a.fn.call({name: ‘B’})

javascript - 图3
this分为几个不同的使用场景,在function中this指的的是window,如果是实用new 调用的话this指的是当前的实例化对象,在事件调用函数中this指的调用事件的dom,特殊的是在IE中的attachEvent中的this总是指向全局对象Window;,在定时器中this指的是window,在es6中有一个箭头函数,在箭头函数中this永远指向的是父级对象,this也是可以改变的,在js中call, apply, bind都可以改变this的指向, call, apply都是执行一个函数并且改变this,区别就是参数传递不一样,而bind是返回一个绑定this之后的

闭包

闭包的形成与变量的作用域以及变量的生存周期有密切的关系

1,变量的作用域
• 在js中我们把作用域分为全局作用域和局部作用域,全局作用域就是window,在没有块级作用域概念的时候,每一个函数都是一个局部作用域。
• 其实变量的作用域,就说指变量的有效范围。我们最长说的就是在函数中声明的变量作用域。
• 当在函数中声明一个变量的时候,如果该变量没有用var关键字去定义那么该变量就是一个全局变量,但是这样做最容易造成命名冲突。
• 另一种情况就是使用var声明的变量,这时候的变量就是局部变量,只有在该函数内部可以访问,在函数外面是访问不到的
• 在javascript中,函数可以用来创造函数作用域。在函数中搜索变量的时候,如果该函数当中没有这个变量,那么这次搜索过程会随着代码执行环境创建的作用域链往外层逐层搜索,一直搜索到window对象为止,找不到就会抛出一个为定义的错误。而这种从内到外逐层查找的关系在js中我们称为作用域链

2,变量的生存周期
除了变量作用域之外,另外一个跟闭包有关的概念就是变量的生存周期,对于全局变量来说,全局变量的生存周期是永久的,除非我们主动销毁这个全局变量,而对于函数内部的使用var声明的变量来说,当退出函数时,这些变量就会随着函数的结束而销毁。

3,闭包的形成
Javascript允许使用内部函数,可以将函数定义和函数表达式放在另一个函数的函数体内。而且,内部函数可以访问它所在的外部函数声明的局部变量、参数以及声明的其他内部函数。当其中一个这样的内部函数在包含它们的外部函数之外被调用时,就会形成闭包。常见的闭包写法就是简单的函数套函数返回一个函数,通过另一个函数访问这个函数的局部变量,利用闭包可以突破作用域链,将函数内部的变量和方法传递到外部,延续变量的生命。使用闭包可以减少全局环境的污染,也可用延续变量的生命。
自动检测
var $_get = (function () {
var search = location.search.substr(1);
var searchArr = search.split(‘&’)
var searchJSON = {}
searchArr.forEach(item => {
var objItem = item.split(‘=’)
searchJSON[objItem[0]] = objItem[1]
})
console.log(searchJSON)
return function (key) {
return searchJSON[key]
}
})()
$_get(‘aa’)
闭包就是函数嵌套,一个返回一个函数,内部函数在他声明外部作用域被外部调用,就会形成闭包,当形成闭包以后闭包内部的变量就不会随着函数的结束而销毁了,常见的闭包写法就是,一个函数返回一个函数,使用闭包可以延长局部变量的生命周期,并且可以有独立的运行环境不会和全局发生冲突,常见的闭包场景就是模块化

4,闭包的适用场景
闭包的适用场景非常广泛,首先从闭包的优点出发就是:
减少全局环境的污染生成独立的运行环境
模块化就是利用这个特点对不同的模块都有自己独立的运行环境,不会和全局冲突,模块和模块之间通过抛出的接口进行依赖使用
以及像我们常用的jquery类库(避免和全局冲突使用闭包实现自己独立的环境)

可以通过返回其他函数的方式突破作用域链
可以利用这个功能做一些值的缓存工作,例如常见的设计模式(单例模式)

其实当遇到以下场景的时候都可以使用闭包
1.维护函数内的变量安全,避免全局变量的污染。
2.维持一个变量不被回收。
3.封装模块

5,闭包的缺点
由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大。所以在闭包不用之后,将不使用的局部变量删除,使其被回收。在IE中可能导致内存泄露,即无法回收驻留在内存中的元素,这时候需要手动释放。

6,内存泄露
内存泄漏指一块被分配的内存既不能使用,又不能回收,直到浏览器进程结束。

出现原因:
1.循环引用:含有DOM对象的循环引用将导致大部分当前主流浏览器内存泄露。循环 引用,简单来说假如a引用了b,b又引用了a,a和b就构成了循环引用。
2.JS闭包:闭包,函数返回了内部函数还可以继续访问外部方法中定义的私有变量。
3.Dom泄露,当原有的DOM被移除时,子结点引用没有被移除则无法回收。

7,JavaScript垃圾回收机制
Javascript中,如果一个对象不再被引用,那么这个对象就会被GC(garbage collection)回收。如果两个对象互相引用,而不再被第3者所引用,那么这两个互相引用的对象也会被回收。垃圾回收不是时时的,因为其开销比较大,所以垃圾回收器会按照固定的时间间隔周期性的执行。

函数a被b引用,b又被a外的c引用,这就是为什么函数a执行后不会被回收的原因。

8,垃圾回收的两个方法:

标记清除法:
1.垃圾回收机制给存储在内存中的所有变量加上标记,然后去掉环境中的变量以及被环境中变量所引用的变量(闭包)。
2.操作1之后内存中仍存在标记的变量就是要删除的变量,垃圾回收机制将这些带有标记的变量回收。

引用计数法:
1.垃圾回收机制给一个变量一个引用次数,当声明了一个变量并将一个引用类型赋值给该变量的时候这个值的引用次数就加1。
2.当该变量的值变成了另外一个值,则这个值得引用次数减1。
3.当这个值的引用次数变为0的时候,说明没有变量在使用,垃圾回收机制会在运行的时候清理掉引用次数为0的值占用的空间。

面向对象

1,js的面象对象了解多吗?面向对象的特征、构造函数、继承

面向对象也即是OOP,Object Oriented Programming,是计算机的一种编程架构,OOP的基本原则是计算机是由子程序作用的单个或者多个对象组合而成,包含属性和方法的对象是类的实例,但是JavaScript中没有类class的概念(但是es6中新增的class的概念),而是直接使用对象来实现编程。
面向对象的语言具有三个基本特征:封装、继承和多态
封装:
封装的主要目的就是为了隐藏数据信息,包括属性和方法的私有化。封装可以使对象内部的变化对其他对象而言是透明的,对象只对自己的行为负责。对象之间通过暴露API接口来进行通信,其他对象和用户不需要关心API的实现细节,是对象之间的耦合变松散。(解耦)
公有属性,公有方法:属性在构造函数中声明,方法定义在原型上
私有属性,私有方法 : js中没有私有属性的概念我们一般使用_约定的形式表达私有概念
静态属性,静态方法:我们一般吧静态属性静态方法添加到类上直接通过类来调用
自动检测
function User(name, age, id) {
// 定义用户信息
var _id = id; // 私有属性
var _sayHi = function(){console.log(‘hi’);} // 私有方法
this.name = name; // 公有属性
this.age = age; // 公有属性
this.getId = function() {return _id;} // 特权方法
this.sayHi = function(){return _sayHi;} // 特权方法
}
User.name = ‘User’; // 静态属性
User.sortByAge = function (…arguments){
return arguments.sort((a, b)=>{
return a.age - b.age;
})
} // 静态方法
// 定义用户行为
User.prototype.sayWords = function(words){console.log(words);} // 公有方法

重载:
多态是同一个行为具有多个不同表现形式或形态的能力,简单点就是一个api方法有多种用途,类似jquery中,不同的方法传递参数个数,参数类型等,都可以实现不同的效果

2,继承

构造函数继承
自动检测
var Parent = function () {
this.name = “吧吧吧”,
this.age = 100
}
Parent.prototype = {
getName: function () {
alert(this.name)
}
}
var Child = function () {
Parent.call(this)
}
// 相当于吧父类当成一个函数在子类中运行,并且改变this为子类实例,这样就相当于拷贝了父类中构造函数中的属性和方法,实现构造函数继承,但是无法继承原型中的方法和属性
原型链继承
自动检测
var Parent = function () {
this.name = “吧吧吧”,
this.age = 100
}
Parent.prototype = {
getName: function () {
alert(this.name)
}
}
var Child = function () {
}
Child.prototype = new Parent()
// 利用js原型链的特点将子类的原型等于父类的实例,那么子类在原型链访问过程中就可以访问到父类的属性和方法,问题,多个实例共享一个原型, 如果父类中有引用类型数据的话,多个子类其中一个改变其他的都会改变,因为js引用类型的问题会有属性共享的问题

3,javascript创建对象的几种方式?

javascript创建对象简单的说,无非就是使用内置对象或各种自定义对象,当然还可以用JSON;但写法有很多种,也能混合使用。
1、对象字面量的方式
自动检测
var person={
firstname:”Mark”,
lastname:”Yun”,
age:25,
eyecolor:”black”
};
2、用function来模拟无参的构造函数
自动检测
function Person(){}
var person=new Person();//定义一个function,如果使用new”实例化”,function可以看作是一个Class
person.name=“Mark”;
person.age=”25”;
person.work=function(){
alert(person.name+” hello…”);
}
person.work();

3、用function来模拟参构造函数来实现(用this关键字定义构造的上下文属性)
自动检测
function Pet(name,age,hobby){
this.name=name;//this作用域:当前对象
this.age=age;
this.hobby=hobby;
this.eat=function(){
alert(“我叫”+this.name+”,我喜欢”+this.hobby+”,是个程序员”);
}
}
var maidou =new Pet(“麦兜”,25,”coding”);//实例化、创建对象
maidou.eat();//调用eat方法

4、用工厂方式来创建(内置对象)
自动检测
var wcDog =new Object();
wcDog.name=”旺财”;
wcDog.age=3;
wcDog.work=function(){
alert(“我是”+wcDog.name+”,汪汪汪……”);
}
wcDog.work();

5、用原型方式来创建
自动检测
function Dog(){ }
Dog.prototype.name=”旺财”;
Dog.prototype.eat=function(){alert(this.name+”是个吃货”);}
var wangcai =new Dog();
wangcai.eat();

5、用混合方式来创建
自动检测
function Car(name,price){
this.name=name;
this.price=price;
}
Car.prototype.sell=function(){
alert(“我是”+this.name+”,我现在卖”+this.price+”万元”);
}
var camry =new Car(“凯美瑞”,27);
camry.sell();

4,new 的原理是什么?通过 new 的方式创建对象和通过字面量创建有什么区别?

在调用 new 的过程中会发生以上四件事情:

  1. 新生成了一个对象
  2. 链接到原型
  3. 绑定 this
  4. 返回新对象

根据以上几个过程,我们也可以试着来自己实现一个 new
自动检测
function create() {
let obj = {}
let Con = arguments[0]
obj.proto = Con.prototype
let result = Con.apply(obj, arguments)
return result instanceof Object ? result : obj
}

以下是对实现的分析:

  • 创建一个空对象
  • 获取构造函数
  • 设置空对象的原型
  • 绑定 this 并执行构造函数
  • 确保返回值为对象

对于对象来说,其实都是通过 new 产生的,无论是 function Foo() 还是 let a = { b : 1 } 。
对于创建一个对象来说,更推荐使用字面量的方式创建对象(无论性能上还是可读性)。因为你使用 new Object() 的方式创建对象需要通过作用域链一层层找到 Object,但是你使用字面量的方式就没这个问题。
自动检测
function Foo() {}
// function 就是个语法糖
// 内部等同于 new Function()
let a = { b: 1 }
// 这个字面量内部也是使用了 new Object(

首先创建一个空对象,之后让这个对象的proto指向构造函数的prototype,指向之后继承一些里面的属性,之后将对象return出去,并改变它的指向

JS运行机制

JavaScript引擎是单线程运行的,浏览器无论在什么时候都只且只有一个线程在运行JavaScript程序.浏览器的内核是多线程的,它们在内核制控下相互配合以保持同步,一个浏览器至少实现三个常驻线程:javascript引擎线程,GUI渲染线程,浏览器事件触发线程。这些异步线程都会产生不同的异步的事件.

  1. javascript引擎是基于事件驱动单线程执行的,JS引擎一直等待着任务队列中任务的到来,然后加以处理,浏览器无论什么时候都只有一个JS线程在运行JS程序。
    2. GUI渲染线程负责渲染浏览器界面,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。但需要注意 GUI渲染线程与JS引擎是互斥的,当JS引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
    3. 事件触发线程,当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理。这些事件可来自JavaScript引擎当前执行的代码块如setTimeOut、也可来自浏览器内核的其他线程如鼠标点击、AJAX异步请求等,但由于JS的单线程关系所有这些事件都得排队等待JS引擎处理。(当线程中没有执行任何同步代码的前提下才会执行异步代码)

当程序启动时, 一个进程被创建,同时也运行一个线程, 即为主线程,js的运行机制为单线程 javascript - 图4程序中跑两个线程,一个负责程序本身的运行,作为主线程; 另一个负责主线程与其他线程的的通信,被称为“Event Loop 线程” 。每当遇到异步任务,交给 EventLoop 线程,然后自己往后运行,等到主线程运行完后,再去 EventLoop 线程拿结果。

1)所有任务都在主线程上执行,形成一个执行栈(execution context stack)。

2)主线程之外,还存在一个”任务队列”(task queue)。系统把异步任务放到”任务队列”之中,然后继续执行后续的任务。

3)一旦”执行栈”中的所有任务执行完毕,系统就会读取”任务队列”。如果这个时候,异步任务已经结束了等待状态,就会从”任务队列”进入执行栈,恢复执行。

4)主线程不断重复上面的第三步。

“回调函数”(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当异步任务从”任务队列”回到执行栈,回调函数就会执行。”任务队列”是一个先进先出的数据结构,排在前面的事件,优先返回主线程。主线程的读取过程基本上是自动的,只要执行栈一清空,”任务队列”上第一位的事件就自动返回主线程。

主线程从”任务队列”中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop。

从主线程的角度看,一个异步过程包括下面两个要素:

发起函数(或叫注册函数)A
回调函数callbackFn
它们都是在主线程上调用的,其中注册函数用来发起异步过程,回调函数用来处理结果。

异步进程有:

类似onclick等,由浏览器内核的DOM binding模块处理,事件触发时,回调函数添加到任务队列中;
setTimeout等,由浏览器内核的Timer模块处理,时间到达时,回调函数添加到任务队列中;
Ajax,由浏览器内核的Network模块处理,网络请求返回后,添加到任务队列中。

例如setTimeout(fn, 1000),其中的setTimeout就是异步过程的发起函数,fn是回调函数。用一句话概括:工作线程将消息放到消息队列,主线程通过事件循环过程去取消息。

消息队列:消息队列是一个先进先出的队列,它里面存放着各种消息。
事件循环:事件循环是指主线程重复从消息队列中取消息、执行的过程。

javascript - 图5

流程如下:

  1. 主线程读取js代码, 形成相应的堆和执行栈, 执行同步任务
    2. 当主线程遇到异步任务,,指定给异步进程处理, 同时继续执行同步任务
    3. 当异步进程处理完毕后, 将相应的异步任务推入到任务队列首部
    4. 主线程任务处理完毕后,,查询任务队列,则取出一个任务队列推入到主线程的执行栈
    5. 重复执行第2、3、4步,这就称为事件循环

众所周知 JS 是门非阻塞单线程语言,因为在最初 JS 就是为了和浏览器交互而诞生的。如果 JS 是门多线程的语言话,我们在多个线程中处理 DOM 就可能会发生问题(一个线程中新加节点,另一个线程中删除节点),当然可以引入读写锁解决这个问题。

JS 在执行的过程中会产生执行环境,这些执行环境会被顺序的加入到执行栈中。如果遇到异步的代码,会被挂起并加入到 Task(有多种 task) 队列中。一旦执行栈为空,Event Loop 就会从 Task 队列中拿出需要执行的代码并放入执行栈中执行,所以本质上来说 JS 中的异步还是同步行为。
自动检测
console.log(‘script start’);
setTimeout(function() {
console.log(‘setTimeout’);
}, 0);
console.log(‘script end’);

以上代码虽然 setTimeout延时为 0,其实还是异步。这是因为 HTML5 标准规定这个函数第二个参数不得小于 4 毫秒,不足会自动增加。所以 setTimeout还是会在 script end之后打印。
不同的任务源会被分配到不同的 Task 队列中,任务源可以分为 微任务(microtask) 和 宏任务(macrotask)。在 ES6 规范中,microtask 称为 jobs,macrotask 称为 task。
自动检测
console.log(‘script start’);
setTimeout(function() {
console.log(‘setTimeout’);
}, 0);
new Promise((resolve) => {
console.log(‘Promise’)
resolve()
}).then(function() {
console.log(‘promise1’);
}).then(function() {
console.log(‘promise2’);
});
console.log(‘script end’);
// script start => Promise => script end => promise1 => promise2 => setTimeout

以上代码虽然 setTimeout写在 Promise之前,但是因为 Promise属于微任务而 setTimeout属于宏任务,所以会有以上的打印。

微任务包括 process.nextTick,promise,Object.observe,MutationObserver
宏任务包括 script, setTimeout,setInterval,setImmediate,I/O,UI rendering

很多人有个误区,认为微任务快于宏任务,其实是错误的。因为宏任务中包括了 script,浏览器会先执行一个宏任务,接下来有异步代码的话就先执行微任务。

所以正确的一次 Event loop 顺序是这样的

  1. 执行同步代码,这属于宏任务
  2. 执行栈为空,查询是否有微任务需要执行
  3. 执行所有微任务
  4. 必要的话渲染 UI
  5. 然后开始下一轮 Event loop,执行宏任务中的异步代码

    JS-Web-API 知识点与高频考题解析

    BOM
    BOM(浏览器对象模型)是浏览器本身的一些信息的设置和获取,例如获取浏览器的宽度、高度,设置让浏览器跳转到哪个地址。
    navigator: 获取浏览器特性(即俗称的UA)然后识别客户端
    location: 获取网址、协议、path、参数、hash 等
    history: 操作浏览器的历史纪录,(前进,后退等功能)

1,什么是window对象? 什么是document对象?

window:它是一个顶层对象,而不是另一个对象的属性,即浏览器的窗口。
document:代表整个HTML 文档,可用来访问页面中的所有元素
Window 对象表示当前浏览器的窗口,是JavaScript的顶级对象。我们创建的所有对象、函数、变量都是 Window 对象的成员。
Window 对象的方法和属性是在全局范围内有效的。
Document 对象是 HTML 文档的根节点与所有其他节点(元素节点,文本节点,属性节点, 注释节点)
Document 对象使我们可以通过脚本对 HTML 页面中的所有元素进行访问
Document 对象是 Window 对象的一部分,可通过 window.document 属性对其进行访问

2,事件是?IE与火狐的事件机制有什么区别? 如何阻止冒泡?

  1. 我们在网页中的某个操作(有的操作对应多个事件)。例如:当我们点击一个按钮就会产生一个事件。是可以被 JavaScript 侦测到的行为。
    2. 事件处理机制:IE是事件冒泡、Firefox同时支持两种事件模型,也就是:捕获型事件和冒泡型事件;
    3. ev.stopPropagation();(旧ie的方法 ev.cancelBubble = true;)

3,解释一下事件代理

事件代理的原理其实就和作用域链的原理差不多,但是事件代理是利用事件的冒泡原理来实现的,事件代理就是通过给祖先元素添加事件,通过事件目标对象开始向上查找找到匹配的子节点为止,如果找不到则到绑定事件的那个祖先元素为止,找到了就触发事件,并且可以通过js中call和apply来改变触发事件函数中的this为当前绑定节点,也是通过一层一层逐层向上的方式进行匹配查找而触发对应事件,好处就是可以使后添加的dom元素也同样有之前存在元素的事件,jquery中可以使用on,delegate,live实现的,不过在jquery1.7版本以后吧live给废除了,原因就是live绑定事件的祖先元素是整个html页面的根节点,所以性能消耗比较大,在后边的版本中给删除了,使用on,delegate代替
优点:
使代码简洁
减少浏览器的内存占用
缺点: 使用不当会造成事件在不应该触发时触发

4,offsetWidth/offsetHeight,clientWidth/clientHeight与scrollWidth/scrollHeight的区别,

offsetWidth/offsetHeight返回值包含content + padding + border,效果与e.getBoundingClientRect()相同
clientWidth/clientHeight返回值只包含content + padding,如果有滚动条,也不包含滚动条
scrollWidth/scrollHeight返回值包含content + padding + 溢出内容的尺寸

5,focus/blur与focusin/focusout的区别与联系

focus/blur不冒泡,focusin/focusout冒泡
focus/blur兼容性好,focusin/focusout在除FireFox外的浏览器下都保持良好兼容性,如需使用事件托管,可考虑在FireFox下使用事件捕获elem.addEventListener(‘focus’, handler, true)
可获得焦点的元素:
window
链接被点击或键盘操作
表单空间被点击或键盘操作
设置tabindex属性的元素被点击或键盘操作

6,mouseover/mouseout与mouseenter/mouseleave的区别与联系

mouseover/mouseout是标准事件,所有浏览器都支持;mouseenter/mouseleave是IE5.5引入的特有事件后来被DOM3标准采纳,现代标准浏览器也支持
mouseover/mouseout是冒泡事件;mouseenter/mouseleave不冒泡。需要为多个元素监听鼠标移入/出事件时,推荐mouseover/mouseout托管,提高性能
标准事件模型中event.target表示发生移入/出的元素,vent.relatedTarget对应移出/如元素;在老IE中event.srcElement表示发生移入/出的元素,event.toElement表示移出的目标元素,event.fromElement表示移入时的来源元素

7,介绍DOM0,DOM2,DOM3事件处理方式区别

DOM0级事件处理方式:
btn.onclick = func;
btn.onclick = null;
DOM2级事件处理方式:
btn.addEventListener(‘click’, func, false);
btn.removeEventListener(‘click’, func, false);
btn.attachEvent(“onclick”, func);
btn.detachEvent(“onclick”, func);
DOM3级事件处理方式:
eventUtil.addListener(input, “textInput”, func);
eventUtil 是自定义对象,textInput 是DOM3级事件

8,事件的三个阶段

捕获、目标、冒泡

js的冒泡(Bubbling Event)和捕获(Capture Event)的区别
冒泡型事件:事件按照从最特定的事件目标到最不特定的事件目标(document对象)的顺序触发。
捕获型事件(event capturing):事件从最不精确的对象(document 对象)开始触发,然后到最精确(也可以在窗口级别捕获事件,不过必须由开发人员特别指定)。
DOM事件流:同时支持两种事件模型:捕获型事件和冒泡型事件,但是,捕获型事件先发生。两种事件流会触及DOM中的所有对象,从document对象开始,也在document对象结束。
事件捕获
当你使用事件捕获时,父级元素先触发,子级元素后触发,即div先触发,p后触发。
事件冒泡
当你使用事件冒泡时,子级元素先触发,父级元素后触发,即p先触发,div后触发。

阻止冒泡
• 在W3c中,使用stopPropagation()方法
• 在IE下设置cancelBubble = true;
在捕获的过程中stopPropagation();后,后面的冒泡过程也不会发生了。
阻止捕获

阻止事件的默认行为,例如click 后的跳转
• 在W3c中,使用preventDefault()方法;
• 在IE下设置window.event.returnValue = false;

9,介绍事件“捕获”和“冒泡”执行顺序和事件的执行次数?

按照W3C标准的事件:首是进入捕获阶段,直到达到目标元素,再进入冒泡阶段
事件执行次数(DOM2-addEventListener):元素上绑定事件的个数
注意1:前提是事件被确实触发
注意2:事件绑定几次就算几个事件,即使类型和功能完全一样也不会“覆盖”
事件执行顺序:判断的关键是否目标元素
非目标元素:根据W3C的标准执行:捕获->目标元素->冒泡(不依据事件绑定顺序)
目标元素:依据事件绑定顺序:先绑定的事件先执行(不依据捕获冒泡标准)
最终顺序:父元素捕获->目标元素事件1->目标元素事件2->子元素捕获->子元素冒泡->父元素冒泡
注意:子元素事件执行前提 事件确实“落”到子元素布局区域上,而不是简单的具有嵌套关系
在一个DOM上同时绑定两个点击事件:一个用捕获,一个用冒泡。事件会执行几次,先执行冒泡还是捕获?

该DOM上的事件如果被触发,会执行两次(执行次数等于绑定次数)
如果该DOM是目标元素,则按事件绑定顺序执行,不区分冒泡/捕获
如果该DOM是处于事件流中的非目标元素,则先执行捕获,后执行冒泡

10,window.onload 和 document.DOMContentLoaded (注:$(document).ready()) 的区别?

一般情况下,DOMContentLoaded事件要在window.onload之前执行,当DOM树构建完成的时候就会执行DOMContentLoaded事件,而window.onload是在页面载入完成的时候,才执行,这其中包括图片等元素。大多数时候我们只是想在DOM树构建完成后,绑定事件到元素,我们并不需要图片元素,加上有时候加载外域图片的速度非常缓慢。

DOM

讲 DOM 先从 HTML 讲起,讲 HTML 先从 XML 讲起。XML 是一种可扩展的标记语言,所谓可扩展就是它可以描述任何结构化的数据,它是一棵树!

1,documen.write和 innerHTML的区别

document.write只能重绘整个页面
innerHTML可以重绘页面的一部分

2,DOM操作——怎样添加、移除、移动、复制、创建和查找节点?

1)创建新节点
createDocumentFragment() //创建一个DOM片段
createElement() //创建一个具体的元素
createTextNode() //创建一个文本节点
2)添加、移除、替换、插入
appendChild()
removeChild()
replaceChild()
insertBefore() //在已有的子节点前插入一个新的子节点
3)查找
getElementsByTagName() //通过标签名称
getElementsByName() //通过元素的Name属性的值(IE容错能力较强,会得到一个数组,其中包括id等于name值的)
getElementById() //通过元素Id,唯一性

3,attribute和property的区别是什么?

attribute是dom元素在文档中作为html标签拥有的属性;
property就是dom元素在js中作为对象拥有的属性。
所以:
对于html的标准属性来说,attribute和property是同步的,是会自动更新的,
但是对于自定义的属性来说,他们是不同步的,

4,src和href的区别

src用于替换当前元素,href用于在当前文档和引用资源之间确立联系。
src是source的缩写,指向外部资源的位置,指向的内容将会嵌入到文档中当前标签所在位置;在请求src资源时会将其指向的资源下载并应用到文档内,当浏览器解析到该元素时,会暂停其他资源的下载和处理,直到将该资源加载、编译、执行完毕,图片和框架等元素也如此,类似于将所指向资源嵌入当前标签内。这也是为什么将js脚本放在底部而不是头部。
Src source,指向外部资源的位置,如果我们添加浏览器会暂停其他资源的下载和处理,直到该资源加载,编译,执行完毕(图片和框架也是如此),这也就是为什么js脚本要放在底部。
src用于替换当前元素,href用于在当前文档和引入资源之间建立联系。

兼容与优化

1,页面重构怎么操作?

网站重构:在不改变外部行为的前提下,简化结构、添加可读性,而在网站前端保持一致的行为。
也就是说是在不改变UI的情况下,对网站进行优化,在扩展的同时保持一致的UI。
对于传统的网站来说重构通常是:
表格(table)布局改为DIV+CSS
使网站前端兼容于现代浏览器(针对于不合规范的CSS、如对IE6有效的)
对于移动平台的优化
针对于SEO进行优化
深层次的网站重构应该考虑的方面
减少代码间的耦合
让代码保持弹性
严格按规范编写代码
设计可扩展的API
代替旧有的框架、语言(如VB)
增强用户体验

通常来说对于速度的优化也包含在重构中
压缩JS、CSS、image等前端资源(通常是由服务器来解决)
程序的性能优化(如数据读写)
采用CDN来加速资源加载
对于JS DOM的优化
HTTP服务器的文件缓存

2,列举IE与其他浏览器不一样的特性?

1、事件不同之处:
1-1,触发事件的元素被认为是目标(target)。而在 IE 中,目标包含在 event 对象的 srcElement 属性;
1-2,获取字符代码、如果按键代表一个字符(shift、ctrl、alt除外),IE 的 keyCode 会返回字符代码(Unicode),DOM 中按键的代码和字符是分离的,要获取字符代码,需要使用 charCode 属性;
1-3,阻止某个事件的默认行为,IE 中阻止某个事件的默认行为,必须将 returnValue 属性设置为 false,Mozilla 中,需要调用 preventDefault() 方法;
1-4,停止事件冒泡,IE 中阻止事件进一步冒泡,需要设置 cancelBubble 为 true,Mozzilla 中,需要调用 stopPropagation();

3,什么叫优雅降级和渐进增强?

优雅降级:Web站点在所有新式浏览器中都能正常工作,如果用户使用的是老式浏览器,则代码会针对旧版本的IE进行降级处理了,使之在旧式浏览器上以某种形式降级体验却不至于完全不能用。
如:border-shadow

渐进增强:从被所有浏览器支持的基本功能开始,逐步地添加那些只有新版本浏览器才支持的功能,向页面增加不影响基础浏览器的额外样式和功能的。当浏览器支持时,它们会自动地呈现出来并发挥作用。
如:默认使用flash上传,但如果浏览器支持 HTML5 的文件上传功能,则使用HTML5实现更好的体验;

4,说说严格模式的限制

严格模式主要有以下限制:
变量必须声明后再使用
函数的参数不能有同名属性,否则报错
不能使用with语句
不能对只读属性赋值,否则报错
不能使用前缀0表示八进制数,否则报错
不能删除不可删除的属性,否则报错
不能删除变量delete prop,会报错,只能删除属性delete global[prop]
eval不会在它的外层作用域引入变量
eval和arguments不能被重新赋值
arguments不会自动反映函数参数的变化
不能使用arguments.callee
不能使用arguments.caller
禁止this指向全局对象
不能使用fn.caller和fn.arguments获取函数调用的堆栈
增加了保留字(比如protected、static和interface)
设立”严格模式”的目的,主要有以下几个:
消除Javascript语法的一些不合理、不严谨之处,减少一些怪异行为;
消除代码运行的一些不安全之处,保证代码运行的安全;
提高编译器效率,增加运行速度;
为未来新版本的Javascript做好铺垫。
注:经过测试IE6,7,8,9均不支持严格模式。

5,检测浏览器版本版本有哪些方式?

根据 navigator.userAgent // UA.toLowerCase().indexOf(‘chrome’)
根据 window 对象的成员 // ‘ActiveXObject’ in window

6,总结前端性能优化的解决方案

优化原则和方向
性能优化的原则是以更好的用户体验为标准,具体就是实现下面的目标:
多使用内存、缓存或者其他方法
减少 CPU 和GPU 计算,更快展现

优化的方向有两个:
减少页面体积,提升网络加载
优化页面渲染

减少页面体积,提升网络加载
静态资源的压缩合并(JS 代码压缩合并、CSS 代码压缩合并、雪碧图)
静态资源缓存(资源名称加 MD5 戳)
使用 CDN 让资源加载更快

优化页面渲染
CSS 放前面,JS 放后面
懒加载(图片懒加载、下拉加载更多)
减少DOM 查询,对 DOM 查询做缓存
减少DOM 操作,多个操作尽量合并在一起执行(DocumentFragment)
事件节流
尽早执行操作(DOMContentLoaded)
使用 SSR 后端渲染,数据直接输出到 HTML 中,减少浏览器使用 JS 模板渲染页面 HTML 的时间

7,图片懒加载与预加载

图片懒加载的原理就是暂时不设置图片的src属性,而是将图片的url隐藏起来,比如先写在data-src里面,等某些事件触发的时候(比如滚动到底部,点击加载图片)再将图片真实的url放进src属性里面,从而实现图片的延迟加载
图片预加载是指在一些需要展示大量图片的网站,实现图片的提前加载。从而提升用户体验。常用的方式有两种,一种是隐藏在css的background的url属性里面,一种是通过javascript的Image对象设置实例对象的src属性实现图片的预加载。相关代码如下:

CSS预加载图片方式:
自动检测
#preload-01 { background: url(http://domain.tld/image-01.png) no-repeat -9999px -9999px; }
#preload-02 { background: url(http://domain.tld/image-02.png) no-repeat -9999px -9999px; }
#preload-03 { background: url(http://domain.tld/image-03.png) no-repeat -9999px -9999px; }

Javascript预加载图片的方式:
自动检测
function preloadImg(url) {
var img = new Image();
img.src = url;
if(img.complete) {
//接下来可以使用图片了
//do something here
} else {
img.onload = function() {
//接下来可以使用图片了
//do something here
};
}
}

8,描述浏览器的渲染过程,DOM树和渲染树的区别?

浏览器的渲染过程:
解析HTML构建 DOM(DOM树),并行请求 css/image/js
CSS 文件下载完成,开始构建 CSSOM(CSS树)
CSSOM 构建结束后,和 DOM 一起生成 Render Tree(渲染树)
布局(Layout):计算出每个节点在屏幕中的位置
显示(Painting):通过显卡把页面画到屏幕上
DOM树 和 渲染树 的区别:
DOM树与HTML标签一一对应,包括head和隐藏元素
渲染树不包括head和隐藏元素,大段文本的每一个行都是独立节点,每一个节点都有对应的css属性

9,重绘和回流(重排)的区别和关系?

重绘:当渲染树中的元素外观(如:颜色)发生改变,不影响布局时,产生重绘
回流:当渲染树中的元素的布局(如:尺寸、位置、隐藏/状态状态)发生改变时,产生重绘回流
注意:JS获取Layout属性值(如:offsetLeft、scrollTop、getComputedStyle等)也会引起回流。因为浏览器需要通过回流计算最新值
回流必将引起重绘,而重绘不一定会引起回流 。。。

10,如何最小化重绘(repaint)和回流(reflow)?

需要要对元素进行复杂的操作时,可以先隐藏(display:”none”),操作完成后再显示
需要创建多个DOM节点时,使用DocumentFragment创建完后一次性的加入document
缓存Layout属性值,如:var left = elem.offsetLeft; 这样,多次使用 left 只产生一次回流
尽量避免用table布局(table元素一旦触发回流就会导致table里所有的其它元素回流)
避免使用css表达式(expression),因为每次调用都会重新计算值(包括加载页面)
尽量使用 css 属性简写,如:用 border 代替 border-width, border-style, border-color
批量修改元素样式:elem.className 和 elem.style.cssText 代替 elem.style.xxx

11,script 的位置是否会影响首屏显示时间?

在解析 HTML 生成 DOM 过程中,js 文件的下载是并行的,不需要 DOM 处理到 script 节点。因此,script的位置不影响首屏显示的开始时间。
浏览器解析 HTML 是自上而下的线性过程,script作为 HTML 的一部分同样遵循这个原则
因此,script 会延迟 DomContentLoad,只显示其上部分首屏内容,从而影响首屏显示的完成时间

常见js设计模式

js的设计模式推荐看一本书可以达到更好的理解

javascript - 图6

工厂模式

工厂模式分为好几种,这里就不一一讲解了,以下是一个简单工厂模式的例子
自动检测
class Man {
constructor(name) {
this.name = name
}
alertName() {
alert(this.name)
}
}
class Factory {
static create(name) {
return new Man(name)
}
}
Factory.create(‘yck’).alertName()

当然工厂模式并不仅仅是用来 new 出实例
可以想象一个场景。假设有一份很复杂的代码需要用户去调用,但是用户并不关心这些复杂的代码,只需要你提供给我一个接口去调用,用户只负责传递需要的参数,至于这些参数怎么使用,内部有什么逻辑是不关心的,只需要你最后返回我一个实例。这个构造过程就是工厂。
工厂起到的作用就是隐藏了创建实例的复杂度,只需要提供一个接口,简单清晰。
在 Vue 源码中,你也可以看到工厂模式的使用,比如创建异步组件
自动检测
export function createComponent (
Ctor: Class | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array,
tag?: string
): VNode | Array | void {
// 逻辑处理…
const vnode = new VNode(
vue-component-${Ctor.cid}${name ?-${name}: ''},
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
return vnode
}

在上述代码中,我们可以看到我们只需要调用 createComponent传入参数就能创建一个组件实例,但是创建这个实例是很复杂的一个过程,工厂帮助我们隐藏了这个复杂的过程,只需要一句代码调用就能实现功能。

单例模式

单例模式很常用,比如全局缓存、全局状态管理等等这些只需要一个对象,就可以使用单例模式。
单例模式的核心就是保证全局只有一个对象可以访问。因为 JS 是门无类的语言,所以别的语言实现单例的方式并不能套入 JS 中,我们只需要用一个变量确保实例只创建一次就行,以下是如何实现单例模式的例子
自动检测
class Singleton {
constructor() {}
}
Singleton.getInstance = (function() {
let instance
return function() {
if (!instance) {
instance = new Singleton()
}
return instance
}
})()
let s1 = Singleton.getInstance()
let s2 = Singleton.getInstance()
console.log(s1 === s2) // true

在 Vuex 源码中,你也可以看到单例模式的使用,虽然它的实现方式不大一样,通过一个外部变量来控制只安装一次 Vuex
自动检测
let Vue // bind on install
export function install (_Vue) {
if (Vue && _Vue === Vue) {
// …
return
}
Vue = _Vue
applyMixin(Vue)
}

适配器模式

适配器用来解决两个接口不兼容的情况,不需要改变已有的接口,通过包装一层的方式实现两个接口的正常协作。
以下是如何实现适配器模式的例子
自动检测
class Plug {
getName() {
return ‘港版插头’
}
}
class Target {
constructor() {
this.plug = new Plug()
}
getName() {
return this.plug.getName() + ‘ 适配器转二脚插头’
}
}
let target = new Target()
target.getName() // 港版插头 适配器转二脚插头

在 Vue 中,我们其实经常使用到适配器模式。比如父组件传递给子组件一个时间戳属性,组件内部需要将时间戳转为正常的日期显示,一般会使用 computed来做转换这件事情,这个过程就使用到了适配器模式。

装饰模式

装饰模式不需要改变已有的接口,作用是给对象添加功能。就像我们经常需要给手机戴个保护套防摔一样,不改变手机自身,给手机添加了保护套提供防摔功能。
以下是如何实现装饰模式的例子,使用了 ES7 中的装饰器语法
自动检测
function readonly(target, key, descriptor) {
descriptor.writable = false
return descriptor
}
class Test {
@readonly
name = ‘yck’
}
let t = new Test()
t.yck = ‘111’ // 不可修改

在 React 中,装饰模式其实随处可见
自动检测
import { connect } from ‘react-redux’
class MyComponent extends React.Component {
// …
}
export default connect(mapStateToProps)(MyComponent)

代理模式

代理是为了控制对对象的访问,不让外部直接访问到对象。在现实生活中,也有很多代理的场景。比如你需要买一件国外的产品,这时候你可以通过代购来购买产品。
在实际代码中其实代理的场景很多,也就不举框架中的例子了,比如事件代理就用到了代理模式。
自动检测


  • 1

  • 2

  • 3

  • 4

  • 5


因为存在太多的 li,不可能每个都去绑定事件。这时候可以通过给父节点绑定一个事件,让父节点作为代理去拿到真实点击的节点。
发布-订阅模式
发布-订阅模式也叫做观察者模式。通过一对一或者一对多的依赖关系,当对象发生改变时,订阅方都会收到通知。在现实生活中,也有很多类似场景,比如我需要在购物网站上购买一个产品,但是发现该产品目前处于缺货状态,这时候我可以点击有货通知的按钮,让网站在产品有货的时候通过短信通知我。
在实际代码中其实发布-订阅模式也很常见,比如我们点击一个按钮触发了点击事件就是使用了该模式
自动检测



    在 Vue 中,如何实现响应式也是使用了该模式。对于需要实现响应式的对象来说,在 get的时候会进行依赖收集,当改变了对象的属性时,就会触发派发更新。

    存储

    涉及面试题: 1,有几种方式可以实现存储功能,分别有什么优缺点? 2,如何实现跨标签页的sessionStorage? 3,前端如何操作cookie?

    cookie,localStorage,sessionStorage,indexDB

    我们先来通过表格学习下这几种存储方式的区别

    特性 cookie localStorage sessionStorage indexDB
    数据生命周期 一般由服务器生成,可以设置过期时间 除非被清理,否则一直存在 页面关闭就清理 除非被清理,否则一直存在
    数据存储大小 4K 5M 5M 无限
    与服务端通信 每次都会携带在 header 中,对于请求性能影响 不参与 不参与 不参与

    从上表可以看到,cookie 已经不建议用于存储。如果没有大量数据存储需求的话,可以使用 localStorage 和 sessionStorage 。对于不怎么改变的数据尽量使用 localStorage 存储,否则可以用 sessionStorage 存储。
    对于 cookie 来说,我们还需要注意安全性。

    属性 作用
    value 如果用于保存用户登录态,应该将该值加密,不能使用明文的用户标识
    http-only 不能通过 JS 访问 Cookie,减少 XSS 攻击
    secure 只能在协议为 HTTPS 的请求中携带
    same-site 规定浏览器不能在跨域请求中携带 Cookie,减少 CSRF 攻击

    cookie:服务端和客户端都可以进行操作,存储在客户端,每一次ajax发送都会携带,新增fetchApi可以控制cookie是否发送,cookie不允许跨域访问,并且存储大小比较小,对于客户端来说api简单需要自己封装一个实现(客户端操作只提供一个document.cookie)

    session:服务端存储技术,客户端无法操作,session会在客户的cookie中保存一个sessionid用来识别客户端

    localStorage:客户端存储方案,长期存储没有过期时间,有比较详细的api操作,但是只能存储字符串类型数据,需要自己封装一个支持多种数据类型操作,并且可以设置过期时间的api

    sessionStorage:和localStorage的api一样,但是是零时存储,并且只起作用当前标签页,标签页关闭自动销毁,不能跨标签页使用,需要自己结合localStorage做进一步封装

    es6/es7相关

    1,说说对es6的理解(说一下es6,知道es6吗)

    语法糖(箭头函数,类的定义,继承),以及一些新的扩展(数组,字符串,对象,方法等),对作用域的重新定义,以及异步编程的解决方案(promise,async,await)、解构赋值的出现 , 新增的数据类型(symbol),新增数据结构set,map,以及对模块的支持,字符串模版``

    2,ES6常用特性

    变量定义(let和const,可变与不可变,const定义对象的特殊情况)
    解构赋值
    模板字符串
    数组新API(例:Array.from(),entries(),values(),keys())
    箭头函数(rest参数,扩展运算符,::绑定this)
    Set和Map数据结构(set实例成员值唯一存储key值,map实例存储键值对(key-value))
    Promise对象(前端异步解决方案进化史,generator函数,async函数)
    Class语法糖(super关键字)

    3,ES6 箭头函数中的this和普通函数中的有什么不同

    箭头函数是 ES6 中新的函数定义形式,function name(arg1, arg2) {…}可以使用(arg1, arg2) => {…}来定义。箭头函数没有this,他的this永远指向的是上层距离当前函数最近的this
    自动检测
    // JS 普通函数
    var arr = [1, 2, 3]
    arr.map(function (item) {
    console.log(index)
    return item + 1
    })
    // ES6 箭头函数
    const arr = [1, 2, 3]
    arr.map((item, index) => {
    console.log(index)
    return item + 1
    })
    arr.map(item => item + 1)
    function fn() {
    console.log(‘real’, this) // {a: 100} ,该作用域下的 this 的真实的值
    var arr = [1, 2, 3]
    // 普通 JS
    arr.map(function (item) {
    console.log(‘js’, this) // window 。普通函数,这里打印出来的是全局变量,令人费解
    return item + 1
    })
    // 箭头函数
    arr.map(item => {
    console.log(‘es6’, this) // {a: 100} 。箭头函数,这里打印的就是父作用域的 this
    return item + 1
    })
    }
    fn.call({a: 100})

    箭头函数存在的意义,第一写起来更加简洁,第二可以解决 ES6 之前函数执行中this是全局变量的问题,

    4,ES6 模块化如何使用?

    ES6 中模块化语法更加简洁,主要就是两个点抛出export,引入import
    如果只是输出一个唯一的对象,使用export default即可,代码如下
    自动检测
    // 创建 util1.js 文件,内容如
    export default {
    a: 100
    }
    // 创建 index.js 文件,内容如
    import obj from ‘./util1.js’
    console.log(obj)

    如果想要输出许多个对象,就不能用default了,且import时候要加{…},代码如下
    自动检测
    // 创建 util2.js 文件,内容如
    export function fn1() {
    alert(‘fn1’)
    }
    export function fn2() {
    alert(‘fn2’)
    }
    export const obj = {}

    // 创建 index.js 文件,内容如
    import { fn1, fn2, obj } from ‘./util2.js’
    fn1()
    fn2()

    5,Set 和 Map

    Set 和 Map 都是 ES6 中新增的数据结构,是对当前 JS 数组和对象这两种重要数据结构的扩展。由于是新增的数据结构,目前尚未被大规模使用,但是作为前端程序员,提前了解是必须做到的。先总结一下两者最关键的地方:

    • Set 类似于数组,但数组可以允许元素重复,Set 不允许元素重复
    • Map 类似于对象,但普通对象的 key 必须是字符串或者数字,而 Map 的 key 可以是任何数据类型

    Set
    Set 实例不允许元素有重复,可以通过以下示例证明。可以通过一个数组初始化一个 Set 实例,或者通过add添加元素,元素不能重复,重复的会被忽略。
    自动检测
    // 例1
    const set = new Set([1, 2, 3, 4, 4]);
    console.log(set) // Set(4) {1, 2, 3, 4}
    // 例2
    const set = new Set();
    [2, 3, 5, 4, 5, 8, 8].forEach(item => set.add(item));
    for (let item of set) {
    console.log(item);
    }
    // 2 3 5 4 8
    Set 实例的属性和方法有

    • size:获取元素数量。
    • add(value):添加元素,返回 Set 实例本身。
    • delete(value):删除元素,返回一个布尔值,表示删除是否成功。
    • has(value):返回一个布尔值,表示该值是否是 Set 实例的元素。
    • clear():清除所有元素,没有返回值。

    自动检测
    const s = new Set();
    s.add(1).add(2).add(2); // 添加元素
    s.size // 2

    s.has(1) // true
    s.has(2) // true
    s.has(3) // false
    s.delete(2);
    s.has(2) // false
    s.clear();
    console.log(s); // Set(0) {}

    Set 实例的遍历,可使用如下方法

    • keys():返回键名的遍历器。
    • values():返回键值的遍历器。不过由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys()和values()返回结果一致。
    • entries():返回键值对的遍历器。
    • forEach():使用回调函数遍历每个成员。

    自动检测
    let set = new Set([‘aaa’, ‘bbb’, ‘ccc’]);
    for (let item of set.keys()) {
    console.log(item);
    }
    // aaa
    // bbb
    // ccc
    for (let item of set.values()) {
    console.log(item);
    }
    // aaa
    // bbb
    // ccc
    for (let item of set.entries()) {
    console.log(item);
    }
    // [“aaa”, “aaa”]
    // [“bbb”, “bbb”]
    // [“ccc”, “ccc”]

    set.forEach((value, key) => console.log(key + ‘ : ‘ + value))
    // aaa : aaa
    // bbb : bbb
    // ccc : ccc

    Map
    Map 的用法和普通对象基本一致,先看一下它能用非字符串或者数字作为 key 的特性。
    自动检测
    const map = new Map();
    const obj = {p: ‘Hello World’};
    map.set(obj, ‘OK’)
    map.get(obj) // “OK”
    map.has(obj) // true
    map.delete(obj) // true
    map.has(obj) // false

    需要使用new Map()初始化一个实例,下面代码中set get has delete顾名即可思义(下文也会演示)。其中,map.set(obj, ‘OK’)就是用对象作为的 key (不光可以是对象,任何数据类型都可以),并且后面通过map.get(obj)正确获取了。

    Map 实例的属性和方法如下:

    size:获取成员的数量
    set:设置成员 key 和 value
    get:获取成员属性值
    has:判断成员是否存在
    delete:删除成员
    clear:清空所有

    自动检测
    const map = new Map();
    map.set(‘aaa’, 100);
    map.set(‘bbb’, 200);
    map.size // 2
    map.get(‘aaa’) // 100
    map.has(‘aaa’) // true
    map.delete(‘aaa’)
    map.has(‘aaa’) // false
    map.clear()

    Map 实例的遍历方法有:

    • keys():返回键名的遍历器。
    • values():返回键值的遍历器。
    • entries():返回所有成员的遍历器。
    • forEach():遍历 Map 的所有成员。

    自动检测
    const map = new Map();
    map.set(‘aaa’, 100);
    map.set(‘bbb’, 200);
    for (let key of map.keys()) {
    console.log(key);
    }
    // “aaa”
    // “bbb”
    for (let value of map.values()) {
    console.log(value);
    }
    // 100
    // 200
    for (let item of map.entries()) {
    console.log(item[0], item[1]);
    }
    // aaa 100
    // bbb 200
    // 或者
    for (let [key, value] of map.entries()) {
    console.log(key, value);
    }
    // aaa 100
    // bbb 200

    6,map, filter, reduce

    涉及面试题:map, filter, reduce 各自有什么作用?

    map 作用是生成一个新数组,遍历原数组,将每个元素拿出来做一些变换然后放入到新的数组中。
    自动检测
    [1, 2, 3].map(v => v + 1) // -> [2, 3, 4]
    另外 map 的回调函数接受三个参数,分别是当前索引元素,索引,原数组
    自动检测
    [‘1’,’2’,’3’].map(parseInt)

    • 第一轮遍历 parseInt(‘1’, 0) -> 1
    • 第二轮遍历 parseInt(‘2’, 1) -> NaN
    • 第三轮遍历 parseInt(‘3’, 2) -> NaN

    filter 的作用也是生成一个新数组,在遍历数组的时候将返回值为 true 的元素放入新数组,我们可以利用这个函数删除一些不需要的元素,作用就是数组过滤
    自动检测
    let array = [1, 2, 4, 6]
    let newArray = array.filter(item => item !== 6)
    console.log(newArray) // [1, 2, 4]
    和 map 一样,filter 的回调函数也接受三个参数,用处也相同。
    最后我们来讲解 reduce 这块的内容,同时也是最难理解的一块内容。reduce 可以将数组中的元素通过回调函数最终转换为一个值。
    如果我们想实现一个功能将函数里的元素全部相加得到一个值,可能会这样写代码
    自动检测
    const arr = [1, 2, 3]
    let total = 0
    for (let i = 0; i < arr.length; i++) {
    total += arr[i]
    }
    console.log(total) //6

    但是如果我们使用 reduce 的话就可以将遍历部分的代码优化为一行代码
    自动检测
    const arr = [1, 2, 3]
    const sum = arr.reduce((acc, current) => acc + current, 0)
    console.log(sum)

    对于 reduce 来说,它接受两个参数,分别是回调函数和初始值,接下来我们来分解上述代码中 reduce 的过程

    • 首先初始值为 0,该值会在执行第一次回调函数时作为第一个参数传入
    • 回调函数接受四个参数,分别为累计值、当前元素、当前索引、原数组,后三者想必大家都可以明白作用,这里着重分析第一个参数
    • 在一次执行回调函数时,当前值和初始值相加得出结果 1,该结果会在第二次执行回调函数时当做第一个参数传入
    • 所以在第二次执行回调函数时,相加的值就分别是 1 和 2,以此类推,循环结束后得到结果 6

    想必通过以上的解析大家应该明白 reduce 是如何通过回调函数将所有元素最终转换为一个值的,当然 reduce 还可以实现很多功能,接下来我们就通过 reduce 来实现 map 函数
    自动检测
    const arr = [1, 2, 3]
    const mapArray = arr.map(value => value 2)
    const reduceArray = arr.reduce((acc, current) => {
    acc.push(current
    2)
    return acc
    }, [])
    console.log(mapArray, reduceArray) // [2, 4, 6]

    7,ES6 class 和普通构造函数的区别

    class 其实一直是 JS 的关键字(保留字),但是一直没有正式使用,直到 ES6 。 ES6 的 class 就是取代之前构造函数初始化对象的形式,从语法上更加符合面向对象的写法
    1. class 是一种新的语法形式,是class Name {…}这种形式,和函数的写法完全不一样
    2. 两者对比,构造函数函数体的内容要放在 class 中的constructor函数中,constructor即构造器,初始化实例时默认执行
    3. class 中函数的写法是add() {…}这种形式,并没有function关键字
    自动检测
    // JS 构造函数的写法
    function MathHandle(x, y) {
    this.x = x;
    this.y = y;
    }
    MathHandle.aa = ‘111’
    MathHandle.prototype.add = function () {
    return this.x + this.y;
    };
    var m = new MathHandle(1, 2);
    console.log(m.add())
    // 用 ES6 class 的写法
    class MathHandle {
    stati aa = ‘111’
    constructor(x, y) {
    this.x = x;
    this.y = y;
    }
    aa = ‘aaa’
    add() {
    return this.x + this.y;
    }
    }
    const m = new MathHandle(1, 2);
    console.log(m.add())

    而且使用 class 来实现继承就更加简单了
    在class中直接extends关键字就可以实现继承,而不像之前的继承实现有多种不同的实现方式,在es6中就只有一种
    自动检测
    // JS 构造函数实现继承
    // 动物
    function Animal() {
    this.eat = function () {
    console.log(‘animal eat’)
    }
    }
    // 狗
    function Dog() {
    this.bark = function () {
    console.log(‘dog bark’)
    }
    }
    Dog.prototype = new Animal()
    // 哈士奇
    var hashiqi = new Dog()
    // ES6 class 实现继承
    class Animal {
    constructor(name) {
    this.name = name
    }
    eat() {
    console.log(${this.name} eat)
    }
    }
    class Dog extends Animal {
    constructor(name) {
    super(name)
    this.name = name
    }
    say() {
    console.log(${this.name} say)
    }
    }
    const dog = new Dog(‘哈士奇’)
    dog.say()
    dog.eat()

    注意以下两点:
    使用extends即可实现继承,更加符合经典面向对象语言的写法,如 Java
    子类的constructor一定要执行super(),以调用父类的constructor

    8,箭头函数的作用域上下文和 普通函数作用域上下文 的区别

    箭头函数其实只是一个密名函数的语法糖,区别在于普通函数作用域中的this有特定的指向,一般指向window,而箭头函数中的this只有一个指向那就是指当前函数所在的对象,其实现原理其实就是类似于之前编程的时候在函数外围定义that一样,用了箭头函数就不用定义that了直接使用this

    9,Proxy

    涉及面试题:Proxy 可以实现什么功能?

    如果你平时有关注 Vue 的进展的话,可能已经知道了在 Vue3.0 中将会通过 Proxy 来替换原本的 Object.defineProperty 来实现数据响应式。 Proxy 是 ES6 中新增的功能,它可以用来自定义对象中的操作。
    let p = new Proxy(target, handler)
    target 代表需要添加代理的对象,handler 用来自定义对象中的操作,比如可以用来自定义 set或者 get 函数。
    接下来我们通过 Proxy 来实现一个数据响应式
    自动检测
    let onWatch = (obj, setBind, getLogger) => {
    let handler = {
    get(target, property,receiver{ getLogger(target, property)
    return Reflect.get(target, property, receiver)
    },
    set(target, property, value, receiver) {
    setBind(value, property)
    return Reflect.set(target, property, value)
    }
    }
    return new Proxy(obj, handler)
    }
    let obj = { a: 1 }
    let p = onWatch(
    obj,
    (v, property) => {
    console.log(监听到属性${property}改变为${v})
    },
    (target, property) => {
    console.log('${property}' = ${target[property]})
    }
    )
    p.a = 2 // 监听到属性a改变
    p.a // ‘a’ = 2
    在上述代码中,我们通过自定义 set 和 get 函数的方式,在原本的逻辑中插入了我们的函数逻辑,实现了在对对象任何属性进行读写时发出通知。
    当然这是简单版的响应式实现,如果需要实现一个 Vue 中的响应式,需要我们在 get 中收集依赖,在 set 派发更新,之所以 Vue3.0 要使用 Proxy 替换原本的 API 原因在于 Proxy 无需一层层递归为每个属性添加代理,一次即可完成以上操作,性能上更好,并且原本的实现有一些数据更新不能监听到,但是 Proxy 可以完美监听到任何方式的数据改变,唯一缺陷可能就是浏览器的兼容性不好了。

    Proxy 无需一层层递归为每个属性添加代理有疑问,以下是实现代码。

    自动检测
    get(target, property, receiver) {
    getLogger(target, property)
    // 这句判断代码是新增的
    if (typeof target[property] === ‘object’ && target[property] !== null) {
    return new Proxy(target[property], handler);
    } else {
    return Reflect.get(target, property);
    }
    }

    10,Proxy 与 Object.defineProperty 对比

    Object.defineProperty虽然已经能够实现双向绑定了,但是他还是有缺陷的。

    1. 只能对属性进行数据劫持,所以需要深度遍历整个对象
    2. 对于数组不能监听到数据的变化

    虽然 Vue 中确实能检测到数组数据的变化,但是其实是使用了 hack 的办法,并且也是有缺陷的。
    自动检测
    const arrayProto = Array.prototype
    export const arrayMethods = Object.create(arrayProto)
    // hack 以下几个函数
    const methodsToPatch = [
    ‘push’,
    ‘pop’,
    ‘shift’,
    ‘unshift’,
    ‘splice’,
    ‘sort’,
    ‘reverse’
    ]
    methodsToPatch.forEach(function (method) {
    // 获得原生函数
    const original = arrayProto[method]
    def(arrayMethods, method, function mutator (…args) {
    // 调用原生函数
    const result = original.apply(this, args)
    const ob = this.ob
    let inserted
    switch (method) {
    case ‘push’:
    case ‘unshift’:
    inserted = args
    break
    case ‘splice’:
    inserted = args.slice(2)
    break
    }
    if (inserted) ob.observeArray(inserted)
    // 触发更新
    ob.dep.notify()
    return result
    })
    })

    反观 Proxy 就没以上的问题,原生支持监听数组变化,并且可以直接对整个对象进行拦截,所以 Vue 也将在下个大版本中使用 Proxy 替换 Object.defineProperty
    自动检测
    let onWatch = (obj, setBind, getLogger) => {
    let handler = {
    get(target, property, receiver) {
    getLogger(target, property)
    return Reflect.get(target, property, receiver);
    },
    set(target, property, value, receiver) {
    setBind(value);
    return Reflect.set(target, property, value);
    }
    };
    return new Proxy(obj, handler);
    };
    let obj = { a: 1 }
    let value
    let p = onWatch(obj, (v) => {
    value = v
    }, (target, property) => {
    console.log(Get '${property}' = ${target[property]});
    })
    p.a = 2 // bind value to 2
    p.a // -> Get ‘a’ = 2

    11,es6如何转为es5?

    使用Babel 转码器,Babel 的配置文件是.babelrc,存放在项目的根目录下。使用 Babel 的第一步,就是配置这个文件。

    12,babel

    13,常见面试题

    1. var、let 及 const 区别?
    2. 使用解构,实现两个变量的值的交换?
    3. 解构赋值?
    4. 函数默认参数?
    5. JavaScript 中什么是变量提升?什么是暂时性死区?
    6. 箭头函数?
    7. 箭头函数与普通函数有什么区别?
    8. 反引号 ` 标识?
    9. 属性简写、方法简写?
    10. for of 循环?
    11. 字符串新增方法?
    12. 如何改变函数内部的 this 指针的指向?
    13. 如何判断 this?箭头函数的 this 是什么?
    14. call、apply 以及 bind 函数内部实现是怎么样的?
    15. import 和 export?
    16. ES6 中的 class 了解吗?ES6 中的 class 和 ES5 的类有什么区别?
    17. 知道 ECMAScript6 怎么写 class 么?为什么会出现 class 这种东西?
    18. 原型如何实现继承?Class 如何实现继承?Class 本质是什么?
    19. Promise 有几种状态?Promise 的特点是什么,分别有什么优缺点?
    20. Promise 构造函数是同步还是异步执行?then 呢?Promise 如何实现 then 处理?
    21. Promise 和 setTimeout 的区别?
    22. 如何实现 Promise.all() ?
    23. 如何实现 Promise.prototype.finally() ?
    24. all() 的用法?
    25. es6的展开运算符… 属于浅拷贝还是深拷贝。 答:浅拷贝,只拷贝了最外面的一层数据,如果数组是一个二维数组,拷贝的就是数组的引用
    26. 说说你对 Promise 的了解?

      浏览器相关

      1,跨域

      2,浏览器的渲染原理

      我们知道执行 JS 有一个 JS 引擎,那么执行渲染也有一个渲染引擎。同样,渲染引擎在不同的浏览器中也不是都相同的。比如在 Firefox 中叫做 Gecko,在 Chrome 和 Safari 中都是基于 WebKit 开发的。在这一章节中,我们也会主要学习关于 WebKit 的这部分渲染引擎内容。

    浏览器接收到 HTML 文件并转换为 DOM 树

    当我们打开一个网页时,浏览器都会去请求对应的 HTML 文件。虽然平时我们写代码时都会分为 JS、CSS、HTML 文件,也就是字符串,但是计算机硬件是不理解这些字符串的,所以在网络中传输的内容其实都是 0 和 1 这些字节数据。当浏览器接收到这些字节数据以后,它会将这些字节数据转换为字符串,也就是我们写的代码。

    当数据转换为字符串以后,浏览器会先将这些字符串通过词法分析转换为标记(token),这一过程在词法分析中叫做标记化(tokenization)。

    那么什么是标记呢?这其实属于编译原理这一块的内容了。简单来说,标记还是字符串,是构成代码的最小单位。这一过程会将代码分拆成一块块,并给这些内容打上标记,便于理解这些最小单位的代码是什么意思。
    javascript - 图7
    当结束标记化后,这些标记会紧接着转换为 Node,最后这些 Node 会根据不同 Node 之间的联系构建为一颗 DOM 树。
    javascript - 图8
    以上就是浏览器从网络中接收到 HTML 文件然后一系列的转换过程。
    javascript - 图9
    当然,在解析 HTML 文件的时候,浏览器还会遇到 CSS 和 JS 文件,这时候浏览器也会去下载并解析这些文件,接下来就让我们先来学习浏览器如何解析 CSS 文件。
    将 CSS 文件转换为 CSSOM 树
    其实转换 CSS 到 CSSOM 树的过程和上一小节的过程是极其类似的
    javascript - 图10
    在这一过程中,浏览器会确定下每一个节点的样式到底是什么,并且这一过程其实是很消耗资源的。因为样式你可以自行设置给某个节点,也可以通过继承获得。在这一过程中,浏览器得递归 CSSOM 树,然后确定具体的元素到底是什么样式。
    如果你有点不理解为什么会消耗资源的话,我这里举个例子
    自动检测



    对于第一种设置样式的方式来说,浏览器只需要找到页面中所有的 span 标签然后设置颜色,但是对于第二种设置样式的方式来说,浏览器首先需要找到所有的 span 标签,然后找到 span 标签上的 a 标签,最后再去找到 div 标签,然后给符合这种条件的 span 标签设置颜色,这样的递归过程就很复杂。所以我们应该尽可能的避免写过于具体的 CSS 选择器,然后对于 HTML 来说也尽量少的添加无意义标签,保证层级扁平。

    生成渲染树
    当我们生成 DOM 树和 CSSOM 树以后,就需要将这两棵树组合为渲染树。
    javascript - 图11
    在这一过程中,不是简单的将两者合并就行了。渲染树只会包括需要显示的节点和这些节点的样式信息,如果某个节点是 display: none 的,那么就不会在渲染树中显示。
    当浏览器生成渲染树以后,就会根据渲染树来进行布局(也可以叫做回流),然后调用 GPU 绘制,合成图层,显示在屏幕上。对于这一部分的内容因为过于底层,还涉及到了硬件相关的知识,这里就不再继续展开内容了。
    那么通过以上内容,我们已经详细了解到了浏览器从接收文件到将内容渲染在屏幕上的这一过程。接下来,我们将会来学习上半部分遗留下来的一些知识点。

    为什么操作 DOM 慢
    想必大家都听过操作 DOM 性能很差,但是这其中的原因是什么呢?
    因为 DOM 是属于渲染引擎中的东西,而 JS 又是 JS 引擎中的东西。当我们通过 JS 操作 DOM 的时候,其实这个操作涉及到了两个线程之间的通信,那么势必会带来一些性能上的损耗。操作 DOM 次数一多,也就等同于一直在进行线程之间的通信,并且操作 DOM 可能还会带来重绘回流的情况,所以也就导致了性能上的问题。

    经典面试题:插入几万个 DOM,如何实现页面不卡顿?

    对于这道题目来说,首先我们肯定不能一次性把几万个 DOM 全部插入,这样肯定会造成卡顿,所以解决问题的重点应该是如何分批次部分渲染 DOM。大部分人应该可以想到通过 requestAnimationFrame 的方式去循环的插入 DOM,其实还有种方式去解决这个问题:虚拟滚动(virtualized scroller)。
    这种技术的原理就是只渲染可视区域内的内容,非可见区域的那就完全不渲染了,当用户在滚动的时候就实时去替换渲染的内容。
    javascript - 图12
    从上图中我们可以发现,即使列表很长,但是渲染的 DOM 元素永远只有那么几个,当我们滚动页面的时候就会实时去更新 DOM,这个技术就能顺利解决这道经典面试题。如果你想了解更多的内容可以了解下这个 react-virtualized

    什么情况阻塞渲染
    首先渲染的前提是生成渲染树,所以 HTML 和 CSS 肯定会阻塞渲染。如果你想渲染的越快,你越应该降低一开始需要渲染的文件大小,并且扁平层级,优化选择器。
    然后当浏览器在解析到 script 标签时,会暂停构建 DOM,完成后才会从暂停的地方重新开始。也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件,这也是都建议将 script 标签放在 body 标签底部的原因。
    当然在当下,并不是说 script 标签必须放在底部,因为你可以给 script 标签添加 defer 或者 async 属性。
    当 script 标签加上 defer 属性以后,表示该 JS 文件会并行下载,但是会放到 HTML 解析完成后顺序执行,所以对于这种情况你可以把 script 标签放在任意位置。
    对于没有任何依赖的 JS 文件可以加上 async 属性,表示 JS 文件下载和解析不会阻塞渲染。

    重绘(Repaint)和回流(Reflow)
    重绘和回流会在我们设置节点样式时频繁出现,同时也会很大程度上影响性能。
    重绘是当节点需要更改外观而不会影响布局的,比如改变 color 就叫称为重绘
    回流是布局或者几何属性需要改变就称为回流。
    回流必定会发生重绘,重绘不一定会引发回流。回流所需的成本比重绘高的多,改变父节点里的子节点很可能会导致父节点的一系列回流。
    以下几个动作可能会导致性能问题:
    改变 window 大小
    改变字体
    添加或删除样式
    文字改变
    定位或者浮动
    盒模型
    并且很多人不知道的是,重绘和回流其实也和 Eventloop 有关。
    当 Eventloop 执行完 Microtasks 后,会判断 document 是否需要更新,因为浏览器是 60Hz 的刷新率,每 16.6ms 才会更新一次。
    然后判断是否有 resize 或者 scroll 事件,有的话会去触发事件,所以 resize 和 scroll事件也是至少 16ms 才会触发一次,并且自带节流功能。
    判断是否触发了 media query
    更新动画并且发送事件
    判断是否有全屏操作事件
    执行 requestAnimationFrame 回调
    执行 IntersectionObserver 回调,该方法用于判断元素是否可见,可以用于懒加载上,但是兼容性不好
    更新界面
    以上就是一帧中可能会做的事情。如果在一帧中有空闲时间,就会去执行 requestIdleCallback 回调。
    以上内容来自于 HTML 文档
    既然我们已经知道了重绘和回流会影响性能,那么接下来我们将会来学习如何减少重绘和回流的次数。

    减少重绘和回流

    使用 transform 替代 top
    自动检测




    使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局)

    不要把节点的属性值放在一个循环里当成循环里的变量
    自动检测
    for(let i = 0; i < 1000; i++) {
    // 获取 offsetTop 会导致回流,因为需要去获取正确的值
    console.log(document.querySelector(‘.test’).style.offsetTop)
    }
    不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局

    动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame

    CSS 选择符从右往左匹配查找,避免节点层级过多

    将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点。比如对于 video 标签来说,浏览器会自动将该节点变为图层。
    javascript - 图13
    设置节点为图层的方式有很多,我们可以通过以下几个常用属性可以生成新图层
    will-change
    video、iframe 标签

    思考题

    思考题:在不考虑缓存和优化网络协议的前提下,考虑可以通过哪些方式来最快的渲染页面,也就是常说的关键渲染路径,这部分也是性能优化中的一块内容。

    首先你可能会疑问,那怎么测量到底有没有加快渲染速度呢
    javascript - 图14
    当发生 DOMContentLoaded 事件后,就会生成渲染树,生成渲染树就可以进行渲染了,这一过程更大程度上和硬件有关系了。
    提示如何加速:
    从文件大小考虑
    从 script 标签使用上来考虑
    从 CSS、HTML 的代码书写上来考虑
    从需要下载的内容是否需要在首屏使用上来考虑

    3,从输入 URL 到页面加载全过程

    1. 首先做 DNS 查询,如果这一步做了智能 DNS 解析的话,会提供访问速度最快的 IP 地址回来
    2. 接下来是 TCP 握手,应用层会下发数据给传输层,这里 TCP 协议会指明两端的端口号,然后下发给网络层。网络层中的 IP 协议会确定 IP 地址,并且指示了数据传输中如何跳转路由器。然后包会再被封装到数据链路层的数据帧结构中,最后就是物理层面的传输了
    3. TCP 握手结束后会进行 TLS 握手,然后就开始正式的传输数据
    4. 数据在进入服务端之前,可能还会先经过负责负载均衡的服务器,它的作用就是将请求合理的分发到多台服务器上,这时假设服务端会响应一个 HTML 文件
    5. 首先浏览器会判断状态码是什么,如果是 200 那就继续解析,如果 400 或 500 的话就会报错,如果 300 的话会进行重定向,这里会有个重定向计数器,避免过多次的重定向,超过次数也会报错
    6. 浏览器开始解析文件,如果是 gzip 格式的话会先解压一下,然后通过文件的编码格式知道该如何去解码文件
    7. 文件解码成功后会正式开始渲染流程,先会根据 HTML 构建 DOM 树,有 CSS 的话会去构建 CSSOM 树。如果遇到 script标签的话,会判断是否存在 async或者 defer,前者会并行进行下载并执行 JS,后者会先下载文件,然后等待 HTML 解析完成后顺序执行,如果以上都没有,就会阻塞住渲染流程直到 JS 执行完毕。遇到文件下载的会去下载文件,这里如果使用 HTTP 2.0 协议的话会极大的提高多图的下载效率。
    8. 初始的 HTML 被完全加载和解析后会触发 DOMContentLoaded事件
    9. CSSOM 树和 DOM 树构建完成后会开始生成 Render 树,这一步就是确定页面元素的布局、样式等等诸多方面的东西
    10. 在生成 Render 树的过程中,浏览器就开始调用 GPU 绘制,合成图层,将内容显示在屏幕上了

      4,浏览器缓存机制

      缓存对于前端性能优化来说是个很重要的点,良好的缓存策略可以降低资源的重复加载提高网页的整体加载速度。
      通常浏览器缓存策略分为两种:强缓存和协商缓存。

    强缓存
    实现强缓存可以通过两种响应头实现:Expires和 Cache-Control。强缓存表示在缓存期间不需要请求,state code为 200
    Expires: Wed, 22 Oct 2018 08:41:00 GMT
    复制代码
    Expires是 HTTP / 1.0 的产物,表示资源会在 Wed, 22 Oct 2018 08:41:00 GMT后过期,需要再次请求。并且 Expires受限于本地时间,如果修改了本地时间,可能会造成缓存失效。
    自动检测
    Cache-control: max-age=30
    Cache-Control出现于 HTTP / 1.1,优先级高于 Expires。该属性表示资源会在 30 秒后过期,需要再次请求。

    协商缓存
    如果缓存过期了,我们就可以使用协商缓存来解决问题。协商缓存需要请求,如果缓存有效会返回 304。
    协商缓存需要客户端和服务端共同实现,和强缓存一样,也有两种实现方式。

    Last-Modified 和 If-Modified-Since
    Last-Modified表示本地文件最后修改日期,If-Modified-Since会将 Last-Modified的值发送给服务器,询问服务器在该日期后资源是否有更新,有更新的话就会将新的资源发送回来。
    但是如果在本地打开缓存文件,就会造成 Last-Modified被修改,所以在 HTTP / 1.1 出现了 ETag。

    ETag 和 If-None-Match
    ETag类似于文件指纹,If-None-Match会将当前 ETag发送给服务器,询问该资源 ETag是否变动,有变动的话就将新的资源发送回来。并且 ETag优先级比 Last-Modified高。

    选择合适的缓存策略
    对于大部分的场景都可以使用强缓存配合协商缓存解决,但是在一些特殊的地方可能需要选择特殊的缓存策略

    • 对于某些不需要缓存的资源,可以使用 Cache-control: no-store,表示该资源不需要缓存
    • 对于频繁变动的资源,可以使用 Cache-Control: no-cache并配合 ETag使用,表示该资源已被缓存,但是每次都会发送请求询问资源是否更新。
    • 对于代码文件来说,通常使用 Cache-Control: max-age=31536000并配合策略缓存使用,然后对文件进行指纹处理,一旦文件名变动就会立刻下载新的文件。

    实际场景应用缓存策略
    单纯了解理论而不付诸于实践是没有意义的,接下来我们来通过几个场景学习下如何使用这些理论。

    1,频繁变动的资源

    对于频繁变动的资源,首先需要使用 Cache-Control: no-cache 使浏览器每次都请求服务器,然后配合 ETag 或者 Last-Modified 来验证资源是否有效。这样的做法虽然不能节省请求数量,但是能显著减少响应数据大小。

    2,代码文件

    这里特指除了 HTML 外的代码文件,因为 HTML 文件一般不缓存或者缓存时间很短。

    一般来说,现在都会使用工具来打包代码,那么我们就可以对文件名进行哈希处理,只有当代码修改后才会生成新的文件名。基于此,我们就可以给代码文件设置缓存有效期一年 Cache-Control: max-age=31536000,这样只有当 HTML 文件中引入的文件名发生了改变才会去下载最新的代码文件,否则就一直使用缓存。

    5,调试

    6,性能优化

    静态资源的压缩合并
    如果不合并,每个都会走一遍之前介绍的请求过程
    自动检测



    如果合并了,就只走一遍请求过程
    自动检测

    静态资源缓存
    通过链接名称控制缓存,,,,.
    自动检测

    只有内容改变的时候,链接名称才会改变
    自动检测

    这个名称不用手动改,可通过前端构建工具根据文件内容,为文件名称添加 MD5 后缀。
    使用 CDN 让资源加载更快
    CDN 会提供专业的加载优化方案,静态资源要尽量放在 CDN 上。例如:
    自动检测

    重绘(Repaint)和回流(Reflow)
    重绘和回流是渲染步骤中的一小节,但是这两个步骤对于性能影响很大。

    • 重绘是当节点需要更改外观而不会影响布局的,比如改变 color就叫称为重绘
    • 回流是布局或者几何属性需要改变就称为回流。

    回流必定会发生重绘,重绘不一定会引发回流。回流所需的成本比重绘高的多,改变深层次的节点很可能导致父节点的一系列回流。
    所以以下几个动作可能会导致性能问题:

    • 改变 window 大小
    • 改变字体
    • 添加或删除样式
    • 文字改变
    • 定位或者浮动
    • 盒模型

    使用 SSR 后端渲染
    可一次性输出 HTML 内容,不用在页面渲染完成之后,再通过 Ajax 加载数据、再渲染。例如使用 smarty、Vue SSR 等。
    CSS 放前面,JS 放后面

    懒加载
    一开始先给为 src 赋值成一个通用的预览图,下拉时候再动态赋值成正式的图片。如下,preview.png是预览图片,比较小,加载很快,而且很多图片都共用这个preview.png,加载一次即可。待页面下拉,图片显示出来时,再去替换src为data-realsrc的值。
    自动检测
    javascript - 图15
    另外,这里为何要用data-开头的属性值?—— 所有 HTML 中自定义的属性,都应该用data-开头,因为data-开头的属性浏览器渲染的时候会忽略掉,提高渲染性能。

    DOM 查询做缓存
    两段代码做一下对比:
    自动检测
    var pList = document.getElementsByTagName(‘p’) // 只查询一个 DOM ,缓存在 pList 中了
    var i
    for (i = 0; i < pList.length; i++) {
    }
    var i
    for (i = 0; i < document.getElementsByTagName(‘p’).length; i++) { // 每次循环,都会查询 DOM ,耗费性能
    }
    总结:DOM 操作,无论查询还是修改,都是非常耗费性能的,应尽量减少。

    合并 DOM 插入
    DOM 操作是非常耗费性能的,因此插入多个标签时,先插入 Fragment 然后再统一插入 DOM。
    自动检测
    var listNode = document.getElementById(‘list’)
    // 要插入 10 个 li 标签
    var frag = document.createDocumentFragment();
    var x, li;
    for(x = 0; x < 10; x++) {
    li = document.createElement(“li”);
    li.innerHTML = “List item “ + x;
    frag.appendChild(li); // 先放在 frag 中,最后一次性插入到 DOM 结构中。
    }
    listNode.appendChild(frag);

    事件节流
    例如要在文字改变时触发一个 change 事件,通过 keyup 来监听。使用节流。
    自动检测
    var textarea = document.getElementById(‘text’)
    var timeoutId
    textarea.addEventListener(‘keyup’, function () {
    if (timeoutId) {
    clearTimeout(timeoutId)
    }
    timeoutId = setTimeout(function () {
    // 触发 change 事件
    }, 100)
    })
    // 尽早执行操作
    window.addEventListener(‘load’, function () {
    // 页面的全部资源加载完才会执行,包括图片、视频等
    })
    document.addEventListener(‘DOMContentLoaded’, function () {
    // DOM 渲染完即可执行,此时图片、视频还可能没有加载完
    })

    DNS 预解析
    DNS 解析也是需要时间的,可以通过预解析的方式来预先获得域名所对应的 IP。
    自动检测

    节流
    考虑一个场景,滚动事件中会发起网络请求,但是我们并不希望用户在滚动过程中一直发起请求,而是隔一段时间发起一次,对于这种情况我们就可以使用节流。
    理解了节流的用途,我们就来实现下这个函数
    自动检测
    // func是用户传入需要防抖的函数
    // wait是等待时间
    const throttle = (func, wait = 50) => {
    // 上一次执行该函数的时间
    let lastTime = 0
    return function(…args) {
    // 当前时间
    let now = +new Date()
    // 将当前时间和上一次执行函数时间对比
    // 如果差值大于设置的等待时间就执行函数
    if (now - lastTime > wait) {
    lastTime = now
    func.apply(this, args)
    }
    }
    }
    setInterval(
    throttle(() => {
    console.log(1)
    }, 500),
    1
    )

    防抖
    考虑一个场景,有一个按钮点击会触发网络请求,但是我们并不希望每次点击都发起网络请求,而是当用户点击按钮一段时间后没有再次点击的情况才去发起网络请求,对于这种情况我们就可以使用防抖。
    理解了防抖的用途,我们就来实现下这个函数
    自动检测
    // func是用户传入需要防抖的函数
    // wait是等待时间
    const debounce = (func, wait = 50) => {
    // 缓存一个定时器id
    let timer = 0
    // 这里返回的函数是每次用户实际调用的防抖函数
    // 如果已经设定过定时器了就清空上一次的定时器
    // 开始一个新的定时器,延迟执行用户传入的方法
    return function(…args) {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
    func.apply(this, args)
    }, wait)
    }
    }

    预加载
    在开发中,可能会遇到这样的情况。有些资源不需要马上用到,但是希望尽早获取,这时候就可以使用预加载。
    预加载其实是声明式的 fetch ,强制浏览器请求资源,并且不会阻塞 onload 事件,可以使用以下代码开启预加载
    自动检测

    预加载可以一定程度上降低首屏的加载时间,因为可以将一些不影响首屏但重要的文件延后加载,唯一缺点就是兼容性不好。

    预渲染
    可以通过预渲染将下载的文件预先在后台渲染,可以使用以下代码开启预渲染
    自动检测

    预渲染虽然可以提高页面的加载速度,但是要确保该页面大概率会被用户在之后打开,否则就是白白浪费资源去渲染。

    懒执行
    懒执行就是将某些逻辑延迟到使用时再计算。该技术可以用于首屏优化,对于某些耗时逻辑并不需要在首屏就使用的,就可以使用懒执行。懒执行需要唤醒,一般可以通过定时器或者事件的调用来唤醒。

    懒加载
    懒加载就是将不关键的资源延后加载。
    懒加载的原理就是只加载自定义区域(通常是可视区域,但也可以是即将进入可视区域)内需要加载的东西。对于图片来说,先设置图片标签的 src 属性为一张占位图,将真实的图片资源放入一个自定义属性中,当进入自定义区域时,就将自定义属性替换为 src 属性,这样图片就会去下载资源,实现了图片懒加载。
    懒加载不仅可以用于图片,也可以使用在别的资源上。比如进入可视区域才开始播放视频等等。

    CDN
    CDN 的原理是尽可能的在各个地方分布机房缓存数据,这样即使我们的根服务器远在国外,在国内的用户也可以通过国内的机房迅速加载资源。
    因此,我们可以将静态资源尽量使用 CDN 加载,由于浏览器对于单个域名有并发请求上限,可以考虑使用多个 CDN 域名。并且对于 CDN 加载静态资源需要注意 CDN 域名要与主站不同,否则每次请求都会带上主站的 Cookie,平白消耗流量。

    计算图片大小
    对于一张 100 100 像素的图片来说,图像上有 10000 个像素点,如果每个像素的值是 RGBA 存储的话,那么也就是说每个像素有 4 个通道,每个通道 1 个字 节(8 位 = 1个字节),所以该图片大小大概为 39KB(10000 1 * 4 / 1024)。
    但是在实际项目中,一张图片可能并不需要使用那么多颜色去显示,我们可以通过减少每个像素的调色板来相应缩小图片的大小。
    了解了如何计算图片大小的知识,那么对于如何优化图片,想必大家已经有 2 个思路了:

    • 减少像素点
    • 减少每个像素点能够显示的颜色

    图片加载优化

    1. 不用图片。很多时候会使用到很多修饰类图片,其实这类修饰图片完全可以用 CSS 去代替。
    2. 对于移动端来说,屏幕宽度就那么点,完全没有必要去加载原图浪费带宽。一般图片都用 CDN 加载,可以计算出适配屏幕的宽度,然后去请求相应裁剪好的图片。
    3. 小图使用 base64 格式
    4. 将多个图标文件整合到一张图片中(雪碧图)
    5. 选择正确的图片格式:
    6. 对于能够显示 WebP 格式的浏览器尽量使用 WebP 格式。因为 WebP 格式具有更好的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量,缺点就是兼容性并不好
    7. 小图使用 PNG,其实对于大部分图标这类图片,完全可以使用 SVG 代替
    8. 照片使用 JPEG

    其他文件优化
    CSS 文件放在 head 中
    服务端开启文件压缩功能
    将 script 标签放在 body 底部,因为 JS 文件执行会阻塞渲染。当然也可以把 script 标签放在任意位置然后加上 defer ,表示该文件会并行下载,但是会放到 HTML 解析完成后顺序执行。对于没有任何依赖的 JS 文件可以加上 async ,表示加载和渲染后续文档元素的过程将和 JS 文件的加载与执行并行无序进行。
    执行 JS 代码过长会卡住渲染,对于需要很多时间计算的代码可以考虑使用 Webworker。Webworker 可以让我们另开一个线程执行脚本而不影响渲染。

    使用 Webpack 优化项目
    对于 Webpack4,打包项目使用 production 模式,这样会自动开启代码压缩
    使用 ES6 模块来开启 tree shaking,这个技术可以移除没有使用的代码
    优化图片,对于小图可以使用 base64 的方式写入文件中
    按照路由拆分代码,实现按需加载
    给打包出来的文件名添加哈希,实现浏览器缓存文件

    性能优化怎么做
    上面提到的都是性能优化的单个点,性能优化项目具体实施起来,应该按照下面步骤推进:

    1. 建立性能数据收集平台,摸底当前性能数据,通过性能打点,将上述整个页面打开过程消耗时间记录下来
    2. 分析耗时较长时间段原因,寻找优化点,确定优化目标
    3. 开始优化
    4. 通过数据收集平台记录优化效果
    5. 不断调整优化点和预期目标,循环2~4步骤

    性能优化是个长期的事情,不是一蹴而就的,应该本着先摸底、再分析、后优化的原则逐步来做。

    6,图片格式

    https://juejin.im/post/5ce189e051882525ce3930ee

    7,浏览器缓存

    缓存可以说是性能优化中简单高效的一种优化方式了,它可以显著减少网络传输所带来的损耗
    对于一个数据请求来说,可以分为发起网络请求、后端处理、浏览器响应三个步骤。浏览器缓存可以帮助我们在第一和第三步骤中优化性能。比如说直接使用缓存而不发起请求,或者发起了请求但后端存储的数据和前端一致,那么就没有必要再将数据回传回来,这样就减少了响应数据。
    接下来的内容中我们将通过以下几个部分来探讨浏览器缓存机制:

    • 缓存位置
    • 缓存策略
    • 实际场景应用缓存策略

    缓存位置
    从缓存位置上来说分为四种,并且各自有优先级,当依次查找缓存且都没有命中的时候,才会去请求网络

    1. Service Worker
    2. Memory Cache
    3. Disk Cache
    4. Push Cache
    5. 网络请求

    Service Worker
    在上一章节中我们已经介绍了 Service Worker 的内容,这里就不演示相关的代码了。
    Service Worker 的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的
    当 Service Worker 没有命中缓存的时候,我们需要去调用 fetch 函数获取数据。也就是说,如果我们没有在 Service Worker 命中缓存的话,会根据缓存查找优先级去查找数据。但是不管我们是从 Memory Cache 中还是从网络请求中获取的数据,浏览器都会显示我们是从 Service Worker 中获取的内容。

    Memory Cache
    Memory Cache 也就是内存中的缓存,读取内存中的数据肯定比磁盘快。但是内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。 一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。
    当我们访问过页面以后,再次刷新页面,可以发现很多数据都来自于内存缓存
    从内存中读取缓存
    那么既然内存缓存这么高效,我们是不是能让数据都存放在内存中呢?
    先说结论,这是不可能的。首先计算机中的内存一定比硬盘容量小得多,操作系统需要精打细算内存的使用,所以能让我们使用的内存必然不多。内存中其实可以存储大部分的文件,比如说 JSS、HTML、CSS、图片等等。但是浏览器会把哪些文件丢进内存这个过程就很玄学了,我查阅了很多资料都没有一个定论。
    当然,我通过一些实践和猜测也得出了一些结论:

    • 对于大文件来说,大概率是不存储在内存中的,反之优先
    • 当前系统内存使用率高的话,文件优先存储进硬盘

    Disk Cache
    Disk Cache 也就是存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之 Memory Cache 胜在容量和存储时效性上。
    在所有浏览器缓存中,Disk Cache 覆盖面基本是最大的。它会根据 HTTP Herder 中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。并且即使在跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据。

    Push Cache
    Push Cache 是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。并且缓存时间也很短暂,只在会话(Session)中存在,一旦会话结束就被释放。
    Push Cache 在国内能够查到的资料很少,也是因为 HTTP/2 在国内不够普及,但是 HTTP/2 将会是日后的一个趋势。这里推荐阅读 HTTP/2 push is tougher than I thought 这篇文章,但是内容是英文的,我翻译一下文章中的几个结论,有能力的同学还是推荐自己阅读

    • 所有的资源都能被推送,但是 Edge 和 Safari 浏览器兼容性不怎么好
    • 可以推送 no-cache 和 no-store 的资源
    • 一旦连接被关闭,Push Cache 就被释放
    • 多个页面可以使用相同的 HTTP/2 连接,也就是说能使用同样的缓存
    • Push Cache 中的缓存只能被使用一次
    • 浏览器可以拒绝接受已经存在的资源推送
    • 你可以给其他域名推送资源

    网络请求
    如果所有缓存都没有命中的话,那么只能发起请求来获取资源了。
    那么为了性能上的考虑,大部分的接口都应该选择好缓存策略,接下来我们就来学习缓存策略这部分的内容。

    缓存策略
    通常浏览器缓存策略分为两种:强缓存协商缓存,并且缓存策略都是通过设置 HTTP Header 来实现的。

    强缓存
    强缓存可以通过设置两种 HTTP Header 实现:Expires 和 Cache-Control 。强缓存表示在缓存期间不需要请求,state code 为 200。
    Expires
    Expires: Wed, 22 Oct 2018 08:41:00 GMT
    Expires 是 HTTP/1 的产物,表示资源会在 Wed, 22 Oct 2018 08:41:00 GMT 后过期,需要再次请求。并且 Expires 受限于本地时间,如果修改了本地时间,可能会造成缓存失效。
    Cache-control
    Cache-control: max-age=30
    Cache-Control 出现于 HTTP/1.1,优先级高于 Expires 。该属性值表示资源会在 30 秒后过期,需要再次请求。
    Cache-Control 可以在请求头或者响应头中设置,并且可以组合使用多种指令
    多种指令配合流程图
    从图中我们可以看到,我们可以将多个指令配合起来一起使用,达到多个目的。比如说我们希望资源能被缓存下来,并且是客户端和代理服务器都能缓存,还能设置缓存失效时间等等。
    接下来我们就来学习一些常见指令的作用
    常见指令作用
    协商缓存
    如果缓存过期了,就需要发起请求验证资源是否有更新。协商缓存可以通过设置两种 HTTP Header 实现:Last-Modified 和 ETag 。
    当浏览器发起请求验证资源时,如果资源没有做改变,那么服务端就会返回 304 状态码,并且更新浏览器缓存有效期。
    协商缓存
    Last-Modified 和 If-Modified-Since
    Last-Modified 表示本地文件最后修改日期,If-Modified-Since 会将 Last-Modified 的值发送给服务器,询问服务器在该日期后资源是否有更新,有更新的话就会将新的资源发送回来,否则返回 304 状态码。
    但是 Last-Modified 存在一些弊端:

    • 如果本地打开缓存文件,即使没有对文件进行修改,但还是会造成 Last-Modified 被修改,服务端不能命中缓存导致发送相同的资源
    • 因为 Last-Modified 只能以秒计时,如果在不可感知的时间内修改完成文件,那么服务端会认为资源还是命中了,不会返回正确的资源

    因为以上这些弊端,所以在 HTTP / 1.1 出现了 ETag 。
    ETag 和 If-None-Match
    ETag 类似于文件指纹,If-None-Match 会将当前 ETag 发送给服务器,询问该资源 ETag 是否变动,有变动的话就将新的资源发送回来。并且 ETag 优先级比 Last-Modified 高。
    以上就是缓存策略的所有内容了,看到这里,不知道你是否存在这样一个疑问。如果什么缓存策略都没设置,那么浏览器会怎么处理?
    对于这种情况,浏览器会采用一个启发式的算法,通常会取响应头中的 Date 减去 Last-Modified值的 10% 作为缓存时间。

    实际场景应用缓存策略
    单纯了解理论而不付诸于实践是没有意义的,接下来我们来通过几个场景学习下如何使用这些理论。

    频繁变动的资源
    对于频繁变动的资源,首先需要使用 Cache-Control: no-cache 使浏览器每次都请求服务器,然后配合 ETag 或者 Last-Modified 来验证资源是否有效。这样的做法虽然不能节省请求数量,但是能显著减少响应数据大小。

    代码文件
    这里特指除了 HTML 外的代码文件,因为 HTML 文件一般不缓存或者缓存时间很短。
    一般来说,现在都会使用工具来打包代码,那么我们就可以对文件名进行哈希处理,只有当代码修改后才会生成新的文件名。基于此,我们就可以给代码文件设置缓存有效期一年 Cache-Control: max-age=31536000,这样只有当 HTML 文件中引入的文件名发生了改变才会去下载最新的代码文件,否则就一直使用缓存。

    前端安全

    1,XSS(Cross Site Scripting,跨站脚本攻击)

    这是前端最常见的攻击方式,很多大型网站(如 Facebook)都被 XSS 攻击过。

    举一个例子,我在一个博客网站正常发表一篇文章,输入汉字、英文和图片,完全没有问题。但是如果我写的是恶意的 JS 脚本,例如获取到document.cookie然后传输到自己的服务器上,那我这篇博客的每一次浏览都会执行这个脚本,都会把访客 cookie 中的信息偷偷传递到我的服务器上来。

    其实原理上就是黑客通过某种方式(发布文章、发布评论等)将一段特定的 JS 代码隐蔽地输入进去。然后别人再看这篇文章或者评论时,之前注入的这段 JS 代码就执行了。JS 代码一旦执行,那可就不受控制了,因为它跟网页原有的 JS 有同样的权限,例如可以获取 server 端数据、可以获取 cookie 等。于是,攻击就这样发生了。

    XSS的危害
    XSS 的危害相当大,如果页面可以随意执行别人不安全的 JS 代码,轻则会让页面错乱、功能缺失,重则会造成用户的信息泄露。

    比如早些年社交网站经常爆出 XSS 蠕虫,通过发布的文章内插入 JS,用户访问了感染不安全 JS 注入的文章,会自动重新发布新的文章,这样的文章会通过推荐系统进入到每个用户的文章列表面前,很快就会造成大规模的感染。

    还有利用获取 cookie 的方式,将 cookie 传入入侵者的服务器上,入侵者就可以模拟 cookie 登录网站,对用户的信息进行篡改。

    XSS的预防
    那么如何预防 XSS 攻击呢?—— 最根本的方式,就是对用户输入的内容进行验证和替换,需要替换的字符有:

    & 替换为:&
    < 替换为:<
    > 替换为:>
    ” 替换为:"
    ‘ 替换为:'
    / 替换为:/
    替换了这些字符之后,黑客输入的攻击代码就会失效,XSS 攻击将不会轻易发生。

    除此之外,还可以通过对 cookie 进行较强的控制,比如对敏感的 cookie 增加http-only限制,让 JS 获取不到 cookie 的内容。

    2,CSRF(Cross-site request forgery,跨站请求伪造)

    CSRF 是借用了当前操作者的权限来偷偷地完成某个操作,而不是拿到用户的信息。

    例如,一个支付类网站,给他人转账的接口是http://buy.com/pay?touid=999&money=100,而这个接口在使用时没有任何密码或者 token 的验证,只要打开访问就直接给他人转账。一个用户已经登录了http://buy.com,在选择商品时,突然收到一封邮件,而这封邮件正文有这么一行代码javascript - 图16,他访问了邮件之后,其实就已经完成了购买。

    CSRF 的发生其实是借助了一个 cookie 的特性。我们知道,登录了http://buy.com之后,cookie 就会有登录过的标记了,此时请求http://buy.com/pay?touid=999&money=100是会带着 cookie 的,因此 server 端就知道已经登录了。而如果在http://buy.com去请求其他域名的 API 例如http://abc.com/api时,是不会带 cookie 的,这是浏览器的同源策略的限制。但是 —— 此时在其他域名的页面中,请求http://buy.com/pay?touid=999&money=100,会带着buy.com的 cookie ,这是发生 CSRF 攻击的理论基础。

    预防 CSRF 就是加入各个层级的权限验证,例如现在的购物网站,只要涉及现金交易,肯定要输入密码或者指纹才行。除此之外,敏感的接口使用POST请求而不是GET也是很重要的。

    3,XSS

    涉及面试题:什么是 XSS 攻击?如何防范 XSS 攻击?什么是 CSP?

    XSS 简单点来说,就是攻击者想尽一切办法将可以执行的代码注入到网页中。
    XSS 可以分为多种类型,但是总体上我认为分为两类:持久型和非持久型
    持久型也就是攻击的代码被服务端写入进数据库中,这种攻击危害性很大,因为如果网站访问量很大的话,就会导致大量正常访问页面的用户都受到攻击。
    举个例子,对于评论功能来说,就得防范持久型 XSS 攻击,因为我可以在评论中输入以下内容
    javascript - 图17
    这种情况如果前后端没有做好防御的话,这段评论就会被存储到数据库中,这样每个打开该页面的用户都会被攻击到。
    非持久型相比于前者危害就小的多了,一般通过修改 URL 参数的方式加入攻击代码,诱导用户访问链接从而进行攻击。
    举个例子,如果页面需要从 URL 中获取某些参数作为内容的话,不经过过滤就会导致攻击代码被执行

    {{name}}

    但是对于这种攻击方式来说,如果用户使用 Chrome 这类浏览器的话,浏览器就能自动帮助用户防御攻击。但是我们不能因此就不防御此类攻击了,因为我不能确保用户都使用了该类浏览器。
    javascript - 图18
    对于 XSS 攻击来说,通常有两种方式可以用来防御。

    转义字符
    首先,对于用户的输入应该是永远不信任的。最普遍的做法就是转义输入输出的内容,对于引号、尖括号、斜杠进行转义
    自动检测
    function escape(str) {
    str = str.replace(/&/g, ‘&’)
    str = str.replace(/ str = str.replace(/>/g, ‘>’)
    str = str.replace(/“/g, ‘&quto;’)
    str = str.replace(/‘/g, ‘'’)
    str = str.replace(/`/g, ‘`’)
    str = str.replace(/\//g, ‘/’)
    return str
    }
    通过转义可以将攻击代码 变成
    // -> <script>alert(1)</script>
    escape(‘‘)

    但是对于显示富文本来说,显然不能通过上面的办法来转义所有字符,因为这样会把需要的格式也过滤掉。对于这种情况,通常采用白名单过滤的办法,当然也可以通过黑名单过滤,但是考虑到需要过滤的标签和标签属性实在太多,更加推荐使用白名单的方式。
    自动检测
    const xss = require(‘xss’)
    let html = xss(‘

    XSS Demo

    ‘)
    // ->

    XSS Demo

    <script>alert(“xss”);</script>
    console.log(html)

    以上示例使用了 js-xss 来实现,可以看到在输出中保留了 h1 标签且过滤了 script 标签。

    4,CSP

    CSP 本质上就是建立白名单,开发者明确告诉浏览器哪些外部资源可以加载和执行。我们只需要配置规则,如何拦截是由浏览器自己实现的。我们可以通过这种方式来尽量减少 XSS 攻击。
    通常可以通过两种方式来开启 CSP:

    1. 设置 HTTP Header 中的 Content-Security-Policy
    2. 设置 meta 标签的方式

    这里以设置 HTTP Header 来举例
    只允许加载本站资源

    • Content-Security-Policy: default-src ‘self’

    只允许加载 HTTPS 协议图片

    • Content-Security-Policy: img-src https://*

    允许加载任何来源框架

    • Content-Security-Policy: child-src ‘none’

    当然可以设置的属性远不止这些,你可以通过查阅 文档 的方式来学习,这里就不过多赘述其他的属性了。
    对于这种方式来说,只要开发者配置了正确的规则,那么即使网站存在漏洞,攻击者也不能执行它的攻击代码,并且 CSP 的兼容性也不错。
    javascript - 图19

    5,CSRF

    涉及面试题:什么是 CSRF 攻击?如何防范 CSRF 攻击?

    CSRF 中文名为跨站请求伪造。原理就是攻击者构造出一个后端请求地址,诱导用户点击或者通过某些途径自动发起请求。如果用户是在登录状态下的话,后端就以为是用户在操作,从而进行相应的逻辑。
    举个例子,假设网站中有一个通过 GET 请求提交用户评论的接口,那么攻击者就可以在钓鱼网站中加入一个图片,图片的地址就是评论接口
    javascript - 图20
    那么你是否会想到使用 POST 方式提交请求是不是就没有这个问题了呢?其实并不是,使用这种方式也不是百分百安全的,攻击者同样可以诱导用户进入某个页面,在页面中通过表单提交 POST 请求。

    如何防御
    防范 CSRF 攻击可以遵循以下几种规则:

    1. Get 请求不对数据进行修改
    2. 不让第三方网站访问到用户 Cookie
    3. 阻止第三方网站请求接口
    4. 请求时附带验证信息,比如验证码或者 Token

    SameSite
    可以对 Cookie 设置 SameSite 属性。该属性表示 Cookie 不随着跨域请求发送,可以很大程度减少 CSRF 的攻击,但是该属性目前并不是所有浏览器都兼容。
    验证 Referer
    对于需要防范 CSRF 的请求,我们可以通过验证 Referer 来判断该请求是否为第三方网站发起的。
    Token
    服务器下发一个随机 Token,每次发起请求时将 Token 携带上,服务器验证 Token 是否有效。

    6,点击劫持

    涉及面试题:什么是点击劫持?如何防范点击劫持?

    点击劫持是一种视觉欺骗的攻击手段。攻击者将需要攻击的网站通过 iframe 嵌套的方式嵌入自己的网页中,并将 iframe 设置为透明,在页面中透出一个按钮诱导用户点击。
    javascript - 图21
    对于这种攻击方式,推荐防御的方法有两种。

    1,X-FRAME-OPTIONS

    X-FRAME-OPTIONS 是一个 HTTP 响应头,在现代浏览器有一个很好的支持。这个 HTTP 响应头 就是为了防御用 iframe 嵌套的点击劫持攻击。
    该响应头有三个值可选,分别是

    • DENY,表示页面不允许通过 iframe 的方式展示
    • SAMEORIGIN,表示页面可以在相同域名下通过 iframe 的方式展示
    • ALLOW-FROM,表示页面可以在指定来源的 iframe 中展示

      2, JS 防御

      对于某些远古浏览器来说,并不能支持上面的这种方式,那我们只有通过 JS 的方式来防御点击劫持了。
      自动检测





    以上代码的作用就是当通过 iframe 的方式加载页面时,攻击者的网页直接不显示所有内容了。

    9,中间人攻击

    涉及面试题:什么是中间人攻击?如何防范中间人攻击?

    中间人攻击是攻击方同时与服务端和客户端建立起了连接,并让对方认为连接是安全的,但是实际上整个通信过程都被攻击者控制了。攻击者不仅能获得双方的通信信息,还能修改通信信息。
    通常来说不建议使用公共的 Wi-Fi,因为很可能就会发生中间人攻击的情况。如果你在通信的过程中涉及到了某些敏感信息,就完全暴露给攻击方了。
    当然防御中间人攻击其实并不难,只需要增加一个安全通道来传输信息。HTTPS 就可以用来防御中间人攻击,但是并不是说使用了 HTTPS 就可以高枕无忧了,因为如果你没有完全关闭 HTTP 访问的话,攻击方可以通过某些方式将 HTTPS 降级为 HTTP 从而实现中间人攻击。

    算法

    前端常遇见的数据结构:

    • 简单数据结构(必须理解掌握)
    • 有序数据结构:栈、队列、链表,有序数据结构省空间(存储空间小)
    • 无序数据结构:集合、字典、散列表,无序数据结构省时间(读取时间快)
    • 复杂数据结构
    • 树、堆

    对于简单数据结构,在 ES 中对应的是数组(Array)和对象(Object)。可以想一下,数组的存储是有序的,对象的存储是无序的,但是我要在对象中根据key找到一个值是立即返回的,数组则需要查找的过程。
    这里我通过一个真实面试题目来说明介绍下数据结构设计。

    题目:使用 ECMAScript(JS)代码实现一个事件类Event,包含下面功能:绑定事件、解绑事件和派发事件。

    在稍微复杂点的页面中,比如组件化开发的页面,同一个页面由两三个人来开发,为了保证组件的独立性和降低组件间耦合度,我们往往使用「订阅发布模式」,即组件间通信使用事件监听和派发的方式,而不是直接相互调用组件方法,这就是题目要求写的Event类。
    这个题目的核心是一个事件类型对应回调函数的数据设计。为了实现绑定事件,我们需要一个_cache对象来记录绑定了哪些事件。而事件发生的时候,我们需要从_cache中读取出来事件回调,依次执行它们。一般页面中事件派发(读)要比事件绑定(写)多。所以我们设计的数据结构应该尽量地能够在事件发生时,更加快速地找到对应事件的回调函数们,然后执行。
    经过这样一番考虑,我简单写了下代码实现:
    自动检测
    class Event {
    constructor() {
    // 存储事件的数据结构
    // 为了查找迅速,使用了对象(字典)
    this._cache = {};
    }
    // 绑定
    on(type, callback) {
    // 为了按类查找方便和节省空间,
    // 将同一类型事件放到一个数组中
    // 这里的数组是队列,遵循先进先出
    // 即先绑定的事件先触发
    let fns = (this._cache[type] = this._cache[type] || []);
    if (fns.indexOf(callback) === -1) {
    fns.push(callback);
    }
    return this;
    }
    // 触发
    trigger(type, data) {
    let fns = this._cache[type];
    if (Array.isArray(fns)) {
    fns.forEach((fn) => {
    fn(data);
    });
    }
    return this;
    }
    // 解绑
    off(type, callback) {
    let fns = this._cache[type];
    if (Array.isArray(fns)) {
    if (callback) {
    let index = fns.indexOf(callback);
    if (index !== -1) {
    fns.splice(index, 1);
    }
    } else {
    //全部清空
    fns.length = 0;
    }
    }
    return this;
    }
    }
    // 测试用例
    const event = new Event();
    event.on(‘test’, (a) => {
    console.log(a);
    });
    event.trigger(‘test’, ‘hello world’);
    event.off(‘test’);
    event.trigger(‘test’, ‘hello world’);
    类似于树、堆、图这些高级数据结构,前端一般也不会考查太多,但是它们的查找方法却常考,后面介绍。高级数据应该平时多积累,好好理解,比如理解了堆是什么样的数据结构,在面试中遇见的「查找最大的 K 个数」这类算法问题,就会迎刃而解。

    算法的效率是通过算法复杂度来衡量的(大O表示法)

    算法的好坏可以通过算法复杂度来衡量,算法复杂度包括时间复杂度和空间复杂度两个。时间复杂度由于好估算、好评估等特点,是面试中考查的重点。空间复杂度在面试中考查得不多。
    常见的时间复杂度有:

    • 常数阶 O(1)
    • 对数阶 O(logN)
    • 线性阶 O(n)
    • 线性对数阶 O(nlogN)
    • 平方阶 O(n^2)
    • 立方阶 O(n^3)
    • !k次方阶 O(n^k)
    • 指数阶O(2^n)

    随着问题规模 n 的不断增大,上述时间复杂度不断增大,算法的执行效率越低。
    一般做算法复杂度分析的时候,遵循下面的技巧:

    1. 看看有几重循环,一般来说一重就是O(n),两重就是 O(n^2),以此类推
    2. 如果有二分,则为O(logN)
    3. 保留最高项,去除常数项

      题目:分析下面代码的算法复杂度(为了方便,我已经在注释中加了代码分析)

    自动检测
    let i =0; // 语句执行一次
    while (i < n) { // 语句执行 n 次
    console.log(Current i is ${i}); //语句执行 n 次
    i++; // 语句执行 n 次
    }
    根据注释可以得到,算法复杂度为1 + n + n + n = 1 + 3n,去除常数项,为O(n)。
    自动检测
    let number = 1; // 语句执行一次
    while (number < n) { // 语句执行 logN 次
    number = 2; // 语句执行 logN 次
    }
    上面代码while的跳出判断条件是number<n,而循环体内number增长速度是(2^n),所以循环代码实际执行logN次,复杂度为:1 + 2
    logN = O(logN)
    自动检测
    for (let i = 0; i < n; i++) {// 语句执行 n 次
    for (let j = 0; j < n; j++) {// 语句执行 n^2 次
    console.log(‘I am here!’); // 语句执行 n^2 次
    }
    }
    上面代码是两个for循环嵌套,很容易得出复杂度为:O(n^2)

    人人都要掌握的基础算法

    枚举(迭代)和递归是最最简单的算法,也是复杂算法的基础,人人都应该掌握!枚举相对比较简单,我们重点说下递归。
    递归由下面两部分组成:

    1. 递归主体,就是要循环解决问题的代码
    2. 递归的跳出条件,递归不能一直递归下去,需要完成一定条件后跳出

    1,浅拷贝vs深拷贝

    拷贝其实就是对象复制,为了解决对象复制是产生的引用类型问题

    浅拷贝:利用迭代器,循环对象将对象中的所有可枚举属性复制到另一个对象上,但是浅拷贝的有一个问题就是只是拷贝了对象的一级,其他级还如果是引用类型的值的话依旧解决不了

    深拷贝:深拷贝解决了浅拷贝的问题,利用递归的形势便利对象的每一级,实现起来较为复杂,得判断值是数组还是对象,简单的说就是,在内存中存在两个数据结构完全相同又相互独立的数据,将引用型类型进行复制,而不是只复制其引用关系。

    深拷贝:
    这个问题通常可以通过 JSON.parse(JSON.stringify(object))来解决。
    自动检测
    let a = {
    age: 1,
    jobs: {
    first: ‘FE’
    }
    }
    let b = JSON.parse(JSON.stringify(a))
    a.jobs.first = ‘native’
    console.log(b.jobs.first) // FE

    但是该方法也是有局限性的:

    • 会忽略 undefined
    • 会忽略 symbol
    • 不能序列化函数
    • 不能解决循环引用的对象

    自动检测
    let obj = {
    a: 1,
    b: {
    c: 2,
    d: 3,
    },
    }
    obj.c = obj.b
    obj.e = obj.a
    obj.b.c = obj.c
    obj.b.d = obj.b
    obj.b.e = obj.b.c
    let newObj = JSON.parse(JSON.stringify(obj))
    console.log(newObj)

    如果你有这么一个循环引用对象,你会发现你不能通过该方法深拷贝
    javascript - 图22
    在遇到函数、 undefined或者 symbol的时候,该对象也不能正常的序列化
    自动检测
    let a = {
    age: undefined,
    sex: Symbol(‘male’),
    jobs: function() {},
    name: ‘yck’
    }
    let b = JSON.parse(JSON.stringify(a))
    console.log(b) // {name: “yck”}

    你会发现在上述情况中,该方法会忽略掉函数和 undefined。
    但是在通常情况下,复杂数据都是可以序列化的,所以这个函数可以解决大部分问题,并且该函数是内置函数中处理深拷贝性能最快的。当然如果你的数据中含有以上三种情况下,可以使用 lodash 的深拷贝函数
    如果你所需拷贝的对象含有内置类型并且不包含函数,可以使用 MessageChannel
    自动检测
    function structuralClone(obj) {
    return new Promise(resolve => {
    const {port1, port2} = new MessageChannel();
    port2.onmessage = ev => resolve(ev.data);
    port1.postMessage(obj);
    });
    }
    var obj = {a: 1, b: {
    c: b
    }}
    // 注意该方法是异步的
    // 可以处理 undefined 和循环引用对象
    (async () => {
    const clone = await structuralClone(obj)
    })()
    或者:
    自动检测
    function deepClone(o1, o2) {
    for (let k in o2) {
    if (typeof o2[k] === ‘object’) {
    o1[k] = {};
    deepClone(o1[k], o2[k]);
    } else {
    o1[k] = o2[k];
    }
    }
    }
    // 测试用例
    let obj = {
    a: 1,
    b: [1, 2, 3],
    c: {}
    };
    let emptyObj = Object.create(null);
    deepClone(emptyObj, obj);
    console.log(emptyObj.a == obj.a);
    console.log(emptyObj.b == obj.b);

    2,常见的几种数组排序算法JS实现

    1,快速排序
    从给定的数据中,随机抽出一项,这项的左边放所有比它小的,右边放比它大的,然后再分别这两边执行上述操作,采用的是递归的思想,总结出来就是 实现一层,分别给两边递归,设置好出口
    自动检测
    function fastSort(array,head,tail){
    //考虑到给每个分区操作的时候都是在原有的数组中进行操作的,所以这里head,tail来确定分片的位置
    /生成随机项/
    var randomnum = Math.floor(ranDom(head,tail));
    var random = array[randomnum];
    /将小于random的项放置在其左边 策略就是通过一个临时的数组来储存分好区的结果,再到原数组中替换/
    var arrayTemp = [];
    var unshiftHead = 0;
    for(var i = head;i <= tail;i++){
    if(array[i] arrayTemp.unshift(array[i]);
    unshiftHead++;
    }else if(array[i]>random){
    arrayTemp.push(array[i]);
    }
    /当它等于的时候放哪,这里我想选择放到队列的前面,也就是从unshift后的第一个位置放置/
    if(array[i]===random){
    arrayTemp.splice(unshiftHead,0,array[i]);
    }
    }
    /将对应项覆盖原来的记录/
    for(var j = head , u=0;j <= tail;j++,u++){
    array.splice(j,1,arrayTemp[u]);
    }
    /寻找中间项所在的index/
    var nowIndex = array.indexOf(random);
    /设置出口,当要放进去的片段只有2项的时候就可以收工了/
    if(arrayTemp.length <= 2){
    return;
    }
    /递归,同时应用其左右两个区域/
    fastSort(array,head,nowIndex);
    fastSort(array,nowIndex+1,tail);
    }

    2,插入排序
    思想就是在已经排好序的数组中插入到相应的位置,以从小到大排序为例,扫描已经排好序的片段的每一项,如大于,则继续往后,直到他小于一项时,将其插入到这项的前面
    自动检测
    function insertSort(array){
    /start根据已排列好的项数决定/
    var start=1;
    /按顺序,每一项检查已排列好的序列/
    for(var i=start; i /跟已排好序的序列做对比,并插入到合适的位置/
    for(var j=0; j /小于或者等于时(我们是升序)插入到该项前面/
    if(array[i]<=array[j]){
    console.log(array[i]+’ ‘+array[j]);
    array.splice(j,0,array[i]);
    /删除原有项/
    array.splice(i+1,1);
    break;
    }
    }
    }
    }

    3,冒泡排序
    故名思意 ,就是一个个冒泡到最前端或者最后端,主要是通过两两依次比较,以升序为例,如果前一项比后一项大则交换顺序,一直比到最后一对
    自动检测
    function bubbleSort(array){
    /给每个未确定的位置做循环/
    for(var unfix=array.length-1; unfix>0; unfix—){
    /给进度做个记录,比到未确定位置/
    for(var i=0; i if(array[i]>array[i+1]){
    var temp = array[i];
    array.splice(i,1,array[i+1]);
    array.splice(i+1,1,temp);
    }
    }
    }
    }

    4,选择排序
    将当前未确定块的min或者max取出来插到最前面或者后面
    自动检测
    function selectSort(array){
    /给每个插入后的未确定的范围循环,初始是从0开始/
    for(var unfixed=0; unfixed /设置当前范围的最小值和其索引/
    var min = array[unfixed];
    var minIndex = unfixed;
    /在该范围内选出最小值/
    for(var j=unfixed+1; j if(min>array[j]){
    min = array[j];
    minIndex = j;
    }
    }
    /将最小值插入到unfixed,并且把它所在的原有项替换成/
    array.splice(unfixed,0,min);
    array.splice(minIndex+1,1);
    }
    }

    3,写一个数组去重的方法
    自动检测
    /* 方法一:
    1.构建一个新的数组存放结果
    2.for循环中每次从原数组中取出一个元素,用这个元素循环与结果数组对比
    3.若结果数组中没有该元素,则存到结果数组中
    缺陷:不能去重数组中得引用类型的值和NaN
    /
    function unique(array){
    var result = [];
    for(var i = 0;i < array.length; i++){
    if(result.indexOf(array[i]) == -1) {
    result.push(array[i]);
    }
    }
    return result;
    }
    // [1,2,1,2,’1’,’2’,0,’1’,’你好’,’1’,’你好’,NaN,NaN] => [1, 2, “1”, “2”, 0, “你好”,NaN,NaN]
    // [{id: ‘1’}, {id: ‘1’}] => [{id: ‘1’}, {id: ‘1’}]

    //方法二:ES6
    自动检测
    Array.from(new Set(array))
    // [1,2,1,2,’1’,’2’,0,’1’,’你好’,’1’,’你好’,NaN,NaN] => [1, 2, “1”, “2”, 0, “你好”, NaN]

    4,说一下js模板引擎

    模板引擎原理总结起来就是:先获取html中对应的id下得innerHTML,利用开始标签和关闭标签进行字符串切分,其实是将模板划分成两部份内容,一部分是html部分,一部分是逻辑部分,通过区别一些特殊符号比如each、if等来将字符串拼接成函数式的字符串,将两部分各自经过处理后,再次拼接到一起,最后将拼接好的字符串采用new Function()的方式转化成所需要的函数。
    常用的模版引擎主要有,Template.js,handlebars.js

    5,是否了解公钥加密和私钥加密。

    一般情况下是指私钥用于对数据进行签名,公钥用于对签名进行验证;
    HTTP网站在浏览器端用公钥加密敏感数据,然后在服务器端再用私钥解密。

    6,二分查找

    二分查找法主要是解决「在一堆有序的数中找出指定的数」这类问题,不管这些数是一维数组还是多维数组,只要有序,就可以用二分查找来优化。
    二分查找是一种「分治」思想的算法,大概流程如下:

    1. 数组中排在中间的数字 A,与要找的数字比较大小
    2. 因为数组是有序的,所以: a) A 较大则说明要查找的数字应该从前半部分查找 b) A 较小则说明应该从查找数字的后半部分查找
    3. 这样不断查找缩小数量级(扔掉一半数据),直到找完数组为止

      题目:在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

    自动检测
    function Find(target, array) {
    let i = 0;
    let j = array[i].length - 1;
    while (i < array.length && j >= 0) {
    if (array[i][j] < target) {
    i++;
    } else if (array[i][j] > target) {
    j—;
    } else {
    return true;
    }
    }
    return false;
    }
    //测试用例
    console.log(Find(10, [
    [1, 2, 3, 4],
    [5, 9, 10, 11],
    [13, 20, 21, 23]
    ])
    );
    另外笔者在面试中遇见过下面的问题:

    题目:现在我有一个 1~1000 区间中的正整数,需要你猜下这个数字是几,你只能问一个问题:大了还是小了?问需要猜几次才能猜对?

    拿到这个题目,笔者想到的就是电视上面有个「猜价格」的购物节目,在规定时间内猜对价格就可以把实物抱回家。所以问题就是让面试官不停地回答我猜的数字比这个数字大了还是小了。这就是二分查找!
    猜几次呢?其实这个问题就是个二分查找的算法时间复杂度问题,二分查找的时间复杂度是O(logN),所以求log1000的解就是猜的次数。我们知道2^10=1024,所以可以快速估算出:log1000约等于 10,最多问 10 次就能得到这个数!

    7,数组降维(数组扁平化)

    自动检测
    [1, [2], 3].flatMap((v) => v + 1)
    // -> [2, 3, 4]

    如果想将一个多维数组彻底的降维,可以这样实现
    自动检测
    const flattenDeep = (arr) => Array.isArray(arr)
    ? arr.reduce( (a, b) => […a, …flattenDeep(b)] , [])
    : [arr]
    flattenDeep([1, [[2], [3, [4]], 5]])

    8,防抖

    你是否在日常开发中遇到一个问题,在滚动事件中需要做个复杂计算或者实现一个按钮的防二次点击操作。
    这些需求都可以通过函数防抖动来实现。尤其是第一个需求,如果在频繁的事件回调中做复杂计算,很有可能导致页面卡顿,不如将多次计算合并为一次计算,只在一个精确点做操作。
    PS:防抖和节流的作用都是防止函数多次调用。区别在于,假设一个用户一直触发这个函数,且每次触发函数的间隔小于wait,防抖的情况下只会调用一次,而节流的 情况会每隔一定时间(参数wait)调用函数。
    我们先来看一个袖珍版的防抖理解一下防抖的实现:
    自动检测
    // func是用户传入需要防抖的函数
    // wait是等待时间
    const debounce = (func, wait = 50) => {
    // 缓存一个定时器id
    let timer = 0
    // 这里返回的函数是每次用户实际调用的防抖函数
    // 如果已经设定过定时器了就清空上一次的定时器
    // 开始一个新的定时器,延迟执行用户传入的方法
    return function(…args) {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
    func.apply(this, args)
    }, wait)
    }
    }
    // 不难看出如果用户调用该函数的间隔小于wait的情况下,上一次的时间还未到就被清除了,并不会执行函数

    这是一个简单版的防抖,但是有缺陷,这个防抖只能在最后调用。一般的防抖会有immediate选项,表示是否立即调用。这两者的区别,举个栗子来说:

    • 例如在搜索引擎搜索问题的时候,我们当然是希望用户输入完最后一个字才调用查询接口,这个时候适用延迟执行的防抖函数,它总是在一连串(间隔小于wait的)函数触发之后调用。
    • 例如用户给interviewMap点star的时候,我们希望用户点第一下的时候就去调用接口,并且成功之后改变star按钮的样子,用户就可以立马得到反馈是否star成功了,这个情况适用立即执行的防抖函数,它总是在第一次调用,并且下一次调用必须与前一次调用的时间间隔大于wait才会触发。

    下面我们来实现一个带有立即执行选项的防抖函数
    自动检测
    // 这个是用来获取当前时间戳的
    function now() {
    return +new Date()
    }
    /*
    防抖函数,返回函数连续调用时,空闲时间必须大于或等于 wait,func 才会执行

    @param {function} func 回调函数
    @param {number} wait 表示时间窗口的间隔
    @param {boolean} immediate 设置为ture时,是否立即调用函数
    @return {function} 返回客户调用函数
    /
    function debounce (func, wait = 50, immediate = true) {
    let timer, context, args
    // 延迟执行函数
    const later = () => setTimeout(() => {
    // 延迟函数执行完毕,清空缓存的定时器序号
    timer = null
    // 延迟执行的情况下,函数会在延迟函数中执行
    // 使用到之前缓存的参数和上下文
    if (!immediate) {
    func.apply(context, args)
    context = args = null
    }
    }, wait)
    // 这里返回的函数是每次实际调用的函数
    return function(…params) {
    // 如果没有创建延迟执行函数(later),就创建一个
    if (!timer) {
    timer = later()
    // 如果是立即执行,调用函数
    // 否则缓存参数和调用上下文
    if (immediate) {
    func.apply(this, params)
    } else {
    context = this
    args = params
    }
    // 如果已有延迟执行函数(later),调用的时候清除原来的并重新设定一个
    // 这样做延迟函数会重新计时
    } else {
    clearTimeout(timer)
    timer = later()
    }
    }
    }

    整体函数实现的不难,总结一下。

    • 对于按钮防点击来说的实现:如果函数是立即执行的,就立即调用,如果函数是延迟执行的,就缓存上下文和参数,放到延迟函数中去执行。一旦我开始一个定时器,只要我定时器还在,你每次点击我都重新计时。一旦你点累了,定时器时间到,定时器重置为 null,就可以再次点击了。
    • 对于延时执行函数来说的实现:清除定时器ID,如果是延迟调用就调用函数

      9,手写 call、apply 及 bind 函数

      涉及面试题:call、apply 及 bind 函数内部实现是怎么样的?

    首先从以下几点来考虑如何实现这几个函数

    • 不传入第一个参数,那么上下文默认为 window
    • 改变了 this 指向,让新的对象可以执行该函数,并能接受参数

    那么我们先来实现 call
    自动检测
    Function.prototype.myCall = function(context) {
    if (typeof this !== ‘function’) {
    throw new TypeError(‘Error’)
    }
    context = context || window
    context.fn = this
    const args = […arguments].slice(1)
    const result = context.fn(…args)
    delete context.fn
    return result
    }
    以下是对实现的分析:

    • 首先 context 为可选参数,如果不传的话默认上下文为 window
    • 接下来给 context 创建一个 fn 属性,并将值设置为需要调用的函数
    • 因为 call 可以传入多个参数作为调用函数的参数,所以需要将参数剥离出来
    • 然后调用函数并将对象上的函数删除

    以上就是实现 call 的思路,apply 的实现也类似,区别在于对参数的处理,所以就不一一分析思路了
    自动检测
    Function.prototype.myApply = function(context) {
    if (typeof this !== ‘function’) {
    throw new TypeError(‘Error’)
    }
    context = context || window
    context.fn = this
    let result
    // 处理参数和 call 有区别
    if (arguments[1]) {
    result = context.fn(…arguments[1])
    } else {
    result = context.fn()
    }
    delete context.fn
    return result
    }
    bind 的实现对比其他两个函数略微地复杂了一点,因为 bind 需要返回一个函数,需要判断一些边界问题,以下是 bind 的实现
    自动检测
    Function.prototype.myBind = function (context) {
    if (typeof this !== ‘function’) {
    throw new TypeError(‘Error’)
    }
    const _this = this
    const args = […arguments].slice(1)
    // 返回一个函数
    return function F() {
    // 因为返回了一个函数,我们可以 new F(),所以需要判断
    if (this instanceof F) {
    return new _this(…args, …arguments)
    }
    return _this.apply(context, args.concat(…arguments))
    }
    }
    以下是对实现的分析:

    • 前几步和之前的实现差不多,就不赘述了
    • bind 返回了一个函数,对于函数来说有两种方式调用,一种是直接调用,一种是通过 new 的方式,我们先来说直接调用的方式
    • 对于直接调用来说,这里选择了 apply 的方式实现,但是对于参数需要注意以下情况:因为 bind 可以实现类似这样的代码 f.bind(obj, 1)(2),所以我们需要将两边的参数拼接起来,于是就有了这样的实现 args.concat(…arguments)
    • 最后来说通过 new 的方式,在之前的章节中我们学习过如何判断 this,对于 new 的情况来说,不会被任何方式改变 this,所以对于这种情况我们需要忽略传入的 this

      10,为什么 0.1 + 0.2 != 0.3?如何解决这个问题?

    先说原因,因为 JS 采用 IEEE 754 双精度版本(64位),并且只要采用 IEEE 754 的语言都有该问题。

    我们都知道计算机是通过二进制来存储东西的,那么 0.1 在二进制中会表示为

    // (0011) 表示循环
    0.1 = 2^-4 * 1.10011(0011)
    我们可以发现,0.1 在二进制中是无限循环的一些数字,其实不只是 0.1,其实很多十进制小数用二进制表示都是无限循环的。这样其实没什么问题,但是 JS 采用的浮点数标准却会裁剪掉我们的数字。

    IEEE 754 双精度版本(64位)将 64 位分为了三段

    第一位用来表示符号
    接下去的 11 位用来表示指数
    其他的位数用来表示有效位,也就是用二进制表示 0.1 中的 10011(0011)
    那么这些循环的数字被裁剪了,就会出现精度丢失的问题,也就造成了 0.1 不再是 0.1 了,而是变成了 0.100000000000000002

    0.100000000000000002 === 0.1 // true
    那么同样的,0.2 在二进制也是无限循环的,被裁剪后也失去了精度变成了 0.200000000000000002

    0.200000000000000002 === 0.2 // true
    所以这两者相加不等于 0.3 而是 0.300000000000000004

    0.1 + 0.2 === 0.30000000000000004 // true
    那么可能你又会有一个疑问,既然 0.1 不是 0.1,那为什么 console.log(0.1) 却是正确的呢?

    因为在输入内容的时候,二进制被转换为了十进制,十进制又被转换为了字符串,在这个转换的过程中发生了取近似值的过程,所以打印出来的其实是一个近似值,你也可以通过以下代码来验证

    console.log(0.100000000000000002) // 0.1
    那么说完了为什么,最后来说说怎么解决这个问题吧。其实解决的办法有很多,这里我们选用原生提供的方式来最简单的解决问题

    parseFloat((0.1 + 0.2).toFixed(10)) === 0.3 // true

    11,求斐波那契数列(兔子数列)中的第 n 项

    斐波那契数列就是从 0 和 1 开始,后面的数都是前两个数之和
    0,1,1,2,3,5,8,13,21,34,55,89….

    下面的代码中count记录递归的次数,我们看下两种差异性的代码中的count的值:
    自动检测
    let count = 0;
    function fn(n) {
    let cache = {};
    function _fn(n) {
    if (cache[n]) {
    return cache[n];
    }
    count++;
    if (n == 1 || n == 2) {
    return 1;
    }
    let prev = _fn(n - 1);
    cache[n - 1] = prev;
    let next = _fn(n - 2);
    cache[n - 2] = next;
    return prev + next;
    }
    return _fn(n);
    }
    let count2 = 0;
    function fn2(n) {
    count2++;
    if (n == 1 || n == 2) {
    return 1;
    }
    return fn2(n - 1) + fn2(n - 2);
    }
    console.log(fn(20), count); // 6765 20
    console.log(fn2(20), count2); // 6765 13529

    业务场景/编程实现

    1,有一个需求,需要把后端的更新日志展示出来,后端会不定时的更新日志,前端需要实时更新到页面上,但是前端的页面上每次都只显示20条数据,像一个竖向轮播一样一直持续更新

    这个类需求可以看字眼实时,记住和实时相关那么我们就向使用scoket,对于这个需求我们可以回调,通过scoket接受实时数据并且将实时数据添加到日志数组的前面进行实时展示

    2,防抖和节流应用场景

    添加购物车时,如果用户多次点击添加购物车按钮,这时候会不断的向后端发送数据,如果用户连续点击了好几十次甚至上百次,服务器的压力是很巨大的,怎么防止这个问题

    给你一个input框,实现一个搜索功能,需要注意哪些问题?

    3,购物车实现

    4,实现一个兼容的sticky

    https://juejin.im/post/5c871e025188257e5d0ecfc8

    5,前端如何进行seo优化

    • 合理的title、description、keywords:搜索对着三项的权重逐个减小,title值强调重点即可;description把页面内容高度概括,不可过分堆砌关键词;keywords列举出重要关键词。
    • 语义化的HTML代码,符合W3C规范:语义化代码让搜索引擎容易理解网页
    • 重要内容HTML代码放在最前:搜索引擎抓取HTML顺序是从上到下,保证重要内容一定会被抓取
    • 重要内容不要用js输出:爬虫不会执行js获取内容
    • 少用iframe:搜索引擎不会抓取iframe中的内容
    • 非装饰性图片必须加alt
    • 提高网站速度:网站速度是搜索引擎排序的一个重要指标

    6,登录实现

    登录的目的主要是为了区分客户端用户,在客户端一般都需要用到路由拦截判断用户的登录状态,一般都是通过token的形式,token一般都可以使用jsonwebtoken进行生成,常见的几种实现方法是
    1,公共参数
    登录通过账号密码换取token,并且存储在客户端本地存储中,之后所有ajax请求都会携带一个公共的header参数token,将登录换取token发送到服务端,服务端验证token的正确性

    2,cookie形式
    cookie形式比较简单,客户端无需做什么操作,服务端直接操作cookie,生成token,因为cookie会通过ajax请求自动发送到服务端

    登录体验优化的几个场景
    1,登录后的回调页面,一般登录后需要跳转到其他页面,而其他页面决定于用户是在哪一步操作跳转到的登录页面,这个时候我们应该在登录页面的路由上传递一个登录成功的回调地址

    2,登录回退,一般我门在操作过程中需要登录的时候才登录这个时候登录后,用户后退不应该让用户退回到登录页面

    7,如何处理ajax错误状态

    在项目开发中我们一般情况下错误状态都有统一规范,前端的话为了统一,一般我们都需要做公共的错误处理,可以封装ajax请求拦截器,在请求失败的回调中,通过状态码,判断错误类型,并且弹出对应的错误提示内容

    编程实现

    1,使用 localStorage 封装一个 Storage 对象,达到如下效果:

    自动检测
    Storage.set(‘name’, 哈哈哈’) // 设置 name 字段存储的值为’哈哈哈’。
    Storage.set(‘age’, 2, 30);
    Storage.set(‘people’, [‘Oli’, ‘Aman’, ‘Dante’], 60)
    Storage.get(‘name’) // ‘前端一万小时’
    Storage.get(‘age’) // 如果不超过 30 秒,返回数字类型的 2;如果超过 30 秒,返回 undefined,并且 localStorage 里清除 age 字段。
    Storage.get(‘people’) // 如果不超过 60 秒,返回数组; 如果超过 60 秒,返回 undefined。

    2,补全如下函数,判断用户的浏览器类型。

    自动检测
    function isAndroid(){
    // 补全
    }
    function isIphone(){
    // 补全
    }
    function isIpad(){
    // 补全
    }
    function isIOS(){
    // 补全
    }

    3,写一个函数,参数为时间对象毫秒数的字符串格式,返回值为字符串。假设参数为时间对象毫秒数 t,

    根据 t 的时间分别返回如下字符串:
    - 刚刚( t 距当前时间不到1分钟时间间隔);
    - 3分钟前(t 距当前时间大于等于1分钟,小于1小时);
    - 8小时前(t 距离当前时间大于等于1小时,小于24小时);
    - 3天前(t 距离当前时间大于等于24小时,小于30天);
    - 2个月前(t 距离当前时间大于等于30天小于12个月);
    - 8年前(t 距离当前时间大于等于12个月)。
    自动检测
    function friendlyDate(time){
    // 补充
    }
    var str = friendlyDate( ‘1556286683394’ ) //—> x 分钟前(以当前时间为准)
    var str2 = friendlyDate(‘1555521999999’) //—> x 天前(以当前时间为准)

    4,一道经典笔试题

    自动检测
    function Foo() {
    getName = function() { alert(1); }
    return this
    }
    Foo.getName = function() { alert(2); }
    Foo.prototype.getName = function() { alert(3); }
    var getName = function () { alert(4); }
    function getName() { alert(5); }
    // 输出值?
    Foo.getName();
    getName();
    Foo().getName();
    getName();
    new Foo.getName()
    new Foo().getName()
    new new Foo().getName()

    5. 写一个函数,操作数组,返回一个新数组,新数组中只包含正数。

    自动检测
    function filterPositive(arr){
    }
    var arr = [3, -1, 2, true]
    filterPositive(arr)
    console.log(filterPositive(arr)) //—>[3, 2]

    6. 补全代码,实现数组按姓名、年纪、任意字段排序。

    自动检测
    var users = [
    { name: “John”, age: 20, company: “Baidu” },
    { name: “Pete”, age: 18, company: “Alibaba” },
    { name: “Ann”, age: 19, company: “Tecent” }
    ]
    users.sort(byField(‘age’))
    users.sort(byField(‘company’))

    7. 用 splice 函数分别实现 push、pop、shift、unshift 方法。

    如:
    自动检测
    function push(arr, value){
    arr.splice(arr.length, 0, value)
    return arr.length
    }
    var arr = [3, 4, 5]
    arr.push(10) // arr 变成[3,4,5,10],返回 4

    8,简单实现async/await中的async函数

    sync 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里

    9,简单实现promise函数

    https://juejin.im/post/59dd8b3851882578e04aa05e?from=singlemessage

    10,高级扩展

    https://juejin.im/post/5cb0315f518825215e61ec14#heading-3