前端面试题 回顾与复习(更新中)

9,494 阅读30分钟

还没有完全整理好 希望大家见谅 后面逐步优化

原生DOM操作

  • 如需替换 HTML DOM 中的元素,请使用replaceChild(newnode,oldnode)方法
  • 从父元素中删除子元素 parent.removeChild(child);
  • insertBefore(newItem,existingItem) 在指定的已有子节点之前插入新的子节点
  • appendChild(newListItem向元素添加新的子节点,作为最后一个子节点 

事件模型

事件传播指的是发生事件时传播的过程。一共按顺序分为以下三个阶段。

事件流  

第一阶段捕获阶段:从window对象传导到目标节点(从上到下)的过程,直到目标的元素,为截获事件提供机会

第二阶段目标阶段:在当前目标触发的过程 ,目标接受事件

第三阶段冒泡阶段:从目标节点传导回到windowd对象(从下到上)的过程,在这个阶段对事件做出响应。

事件对象:

DOM事件模型中的事件对象常用属性:

  • type用于获取事件类型 
  • target获取事件目标 
  • stopPropagation()阻止事件冒泡 preventDefault()阻止事件默认行为

IE事件模型中的事件对象常用属性:

  • type用于获取事件类型 
  • srcElement获取事件目标 
  • cancelBubble阻止事件冒泡 
  • returnValue阻止事件默认行为

事件委托/代理:

“事件代理”即是把原本需要绑定的事件委托给⽗元素,让⽗元素担当事件监听的职务。事件代理的原理是DOM元素的事件冒泡。

使⽤事件代理的好处是: 

  • 可以提⾼性能 可以⼤量节省内存占⽤ 
  • 减少事件注册

<ul id="parent">
  <li class="child">one</li>
  <li class="child">two</li>
  <li class="child">three</li>
</ul>

<script type="text/javascript">
  //父元素
  var dom= document.getElementById('parent');

  //父元素绑定事件,代理子元素的点击事件
  dom.onclick= function(event) {
    var event= event || window.event;
    var curTarget= event.target || event.srcElement;

    if (curTarget.tagName.toLowerCase() == 'li') {
      //事件处理
    }
  }
</script>

发布订阅模式与观察者模式

参考链接 https://segmentfault.com/a/1190000019722065

观察者模式:

观察者模式:定义了对象间一种一对多的依赖关系,当目标对象 Subject 的状态发生改变时,所有依赖它的对象 Observer 都会得到通知。

简单点:女神有男朋友了,朋友圈晒个图,甜蜜宣言 “老娘成功脱单,希望你们欢喜”。各位潜藏备胎纷纷失恋,只能安慰自己你不是唯一一个。

模式特征 

1) 一个目标者对象 Subject,拥有方法:添加 / 删除 / 通知 Observer; 

2) 多个观察者对象 Observer,拥有方法:接收 Subject 状态变更通知并处理; 

3) 目标对象 Subject 状态变更时,通知所有 Observer。

Subject 添加一系列 Observer, Subject 负责维护与这些 Observer 之间的联系,“你对我有兴趣,我更新就会通知你”。

// 目标者类
class Subject {
    constructor() {
        // 观察者列表
        this.observers = [];
    }
    // 添加
    add(observer) {
        this.observers.push(observer);
    }
    // 删除
    remove(observer) {
        let idx = this.observers.findIndex(item => item === observer);
        idx > -1 && this.observers.splice(idx, 1);
    }
    // 通知
    notify() {
        for (let observer of this.observers) {
            observer.update();
        }
    }
}

// 观察者类
class Observer {
    constructor(name) {
        this.name = name;
    }
    // 目标对象更新时触发的回调
    update() {
        console.log(`目标者通知我更新了,我是:${this.name}`);
    }
}

// 实例化目标者
let subject = new Subject();

// 实例化两个观察者
let obs1 = new Observer('前端开发者');
let obs2 = new Observer('后端开发者');

// 向目标者添加观察者
subject.add(obs1);
subject.add(obs2);

// 目标者通知更新
subject.notify();
// 输出:
// 目标者通知我更新了,我是前端开发者
// 目标者通知我更新了,我是后端开发者

优势

  1. 目标者与观察者,功能耦合度降低,专注自身功能逻辑;
  2. 观察者被动接收更新,时间上解耦,实时接收目标者更新状态。

不完美

观察者模式虽然实现了对象间依赖关系的低耦合,但却不能对事件通知进行细分管控,如 “筛选通知”,“指定主题事件通知” 。

比如上面的例子,仅通知 “前端开发者” ?观察者对象如何只接收自己需要的更新通知?上例中,两个观察者接收目标者状态变更通知后,都执行了 update(),并无区分。

发布订阅模式(Publisher && Subscriber)

发布订阅模式:基于一个事件(主题)通道,希望接收通知的对象 Subscriber 通过自定义事件订阅主题,被激活事件的对象 Publisher 通过发布主题事件的方式通知各个订阅该主题的 Subscriber 对象。

发布订阅模式与观察者模式的不同,“第三者” (事件中心)出现。目标对象并不直接通知观察者,而是通过事件中心来派发通知。在发布订阅模式里,发布者,并不会直接通知订阅者,换句话说,发布者和订阅者,彼此互不相识。

// 事件中心
let pubSub = {
    // 缓存列表
    list: {},
    /*
     * 添加订阅者(订阅函数),将订阅的类型与回调函数加入缓存列表
     * key: 消息的类型
     * fn: 订阅的回调函数
    */
    subscribe: function (key, fn) {
        if (!this.list[key]) {
            this.list[key] = [];
        }
        this.list[key].push(fn);
    },
    //发布消息(发布函数), 依次通知订阅者
    publish: function(key, ...arg) {
        for(let fn of this.list[key]) {
            fn.call(this, ...arg);
        }
    },
    // 取消订阅
    unSubscribe: function (key, fn) {
        let fnList = this.list[key];
        if (!fnList) return false;

        if (!fn) {
            // 不传入指定取消的订阅方法,则清空所有key下的订阅
            fnList && (fnList.length = 0);
        } else {
            fnList.forEach((item, index) => {
                if (item === fn) {
                fnList.splice(index, 1);
            }
        })
        }
    }
}

// 订阅
pubSub.subscribe('onwork', time => {
    console.log(`上班了:${time}`);
})
pubSub.subscribe('offwork', time => {
    console.log(`下班了:${time}`);
})
pubSub.subscribe('launch', time => {
    console.log(`吃饭了:${time}`);
})

// 发布
pubSub.publish('offwork', '18:00:00');
pubSub.publish('launch', '12:00:00');

// 输出
// 下班了:18:00:00
// 吃饭了:12:00:00

观察者模式 VS 发布订阅模式 区别:

类似点

都是定义一个一对多的依赖关系,有关状态发生变更时执行相应的通知。

区别点

发布订阅模式更灵活,是进阶版的观察者模式,指定对应分发。

  1. 观察者模式维护单一事件对应多个依赖该事件的对象关系;
  2. 发布订阅维护多个事件(主题)及依赖各事件(主题)的对象之间的关系;
  3. 观察者模式是目标对象直接触发通知(全部通知),观察对象被迫接收通知。发布订阅模式多了个中间层(事件中心),由其去管理通知广播(只通知订阅对应事件的对象);
  4. 观察者模式对象间依赖关系较强,发布订阅模式中对象之间实现真正的解耦

原型和继承

原型链:

每个对象都有一个私有属性(非标准属性:__proto__) ,指向它的构造函数的原型对象 (prototype)——「它的构造函数的原型对象」也有一个自己的原型对象,以此类推直到一个对象的原型对象为 nullnull 没有原型,null 是原型链中最后一环。

我们需要牢记两点:

①__proto__和constructor属性是对象所独有的;

② prototype属性是函数所独有的,因为函数也是一种对象,所以函数也拥有__proto__和constructor属性。

__proto__属性的作用就是当访问一个对象的属性时,如果该对象内部不存在这个属性,那么就会去它的__proto__属性所指向的那个对象(父对象)里找,一直找,直到__proto__属性的终点null,再往上找就相当于在null上取值,会报错。通过__proto__属性将对象连接起来的这条链路即我们所谓的原型链。

prototype属性的作用就是让该函数所实例化的对象们都可以找到公用的属性和方法,即f1.__proto__ === Foo.prototype。 constructor属性的含义就是指向该对象的构造函数,所有函数(此时看成对象了)最终的构造函数都指向Function。

function Child(){
    this.name = 'lili'
}
const child = new Child()

console.log(child.__proto__ === Child.prototype) // true
console.log(Child.__proto__ === Function.prototype) // true
console.log(child.__proto__.__proto__.__proto__) // null

1 child.__proto__等于什么? 

答案:Child.prototype 

2 Child.__proto__等于什么? 

答案:Function.prototype 

解析:实例的__proto__属性(原型)等于其构造函数的prototype属性。实例child的构造函数为Child,而Child的构造函数为Function,结果就一目了然了。

继承的实现方案

ES5组合继承

//定义一个父类:人
function Person(cai) {
    this.cai = cai;
    this.emotion = ['喜', '怒', '哀', '乐']; //人都有喜怒哀乐
}
//定义原型类方法 将 Person 类中需共享的方法放到 prototype 中,实现复用
Person.prototype.eat = function () {
    console.log('吃' + this.cai);
}

//定义子类:学生,继承了“人”这个类
function Student(cai, studentID) {
    //先调用父类构造器 子类继承父类的属性 需要将this指向父类中的cai
    Person.call(this, cai);
    this.studentID = studentID; // studentID是子类自己的属性
}
Student.prototype = new Person(); //子类继承父类的方法此时 Student.prototype 中的 constructor 被重写了,会导致 stu1.constructor === Person
Student.prototype.constructor = Student; //将 Student 原型对象的 constructor 指针重新指向 Student 本身
//创建子类的实例
var stu1 = new Student('西兰花', 1001);
console.log(stu1.emotion); //['喜', '怒', '哀', '乐']
console.log(stu1.cai); // 西兰花
stu1.eat(); //吃西兰花
console.log(stu1.constructor); //Student

结合原型链继承和借用构造函数继承即组合继承是javascript最常用的继承模式,不过,它也有自己的不足:组合继承无论在什么情况下,都会调用两次父类构造函数。 一次是在创建子类原型的时候,另一次是在子类构造函数内部.子类最终会包含父类对象的全部实例属性,但我们不得不在调用子类构造函数时重写这些属性。

继承实质的区别:

**ES5:**先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。  

**ES6:**先创造父类的实例对象this,(所以必须先调用super方法)然后再用子类的构造函数修改this。

ES6类式继承

Es6 引入了新的关键字实现 class ,除了 class 之外,还有 constructorstaticextendssuper

class Person {
    constructor(cai) {
        this.cai = cai;
        this.emotion = ['喜', '怒', '哀', '乐']; //人都有喜怒哀乐
    }
    eat() {
        console.log('吃' + this.cai);
    }
}

class Student extends Person {
    constructor(cai, studentID) {
        // 指向父类的构造函数
        super(cai);
        this.studentID = studentID;
    }
    showStudentID() {
        console.log(this.studentID)
    }
}
var stu1 = new Student('西兰花', 1001);
console.log(stu1.emotion);
stu1.eat(); //吃西兰花
stu1.showStudentID() //1001

注:

ES6 里的 Class 是通过关键字 extends 实现继承 

子类必须在 constructor 方法中调用 super 方法 ,super的关键字在这里表示父类的构造函数,用来创造父类的this对象, 而子类是没有自己的 this 对象的,需要调用 super 方法,来继承父类的 this 对象,然后对其加工 ,故可知只有调用了 super 之后才可以使用 this 关键字,否则会报错

性能优化:

alt


1)利⽤多个域名来存储⽹站资源:

cdn:内容分发⽹络,基本思路是尽可能避开互联⽹上有可能影响数据传输速度和稳 定性的瓶颈和环节,使内容传输的更快、更稳定。

原因:

  • CDN 缓存更⽅便
  • 突破浏览器并发限制
  • 节约 cookie 带宽
  • 节约主域名的连接数,优化⻚⾯响应速度
  • 防⽌不必要的安全问题

重绘(Repaint)和回流(Reflow)是啥?如何优化?

  • 重绘是当节点需要更改外观而不会影响布局的,比如改变 color 就叫称为重绘。
  • 回流是布局或者几何属性需要改变就称为回流。 回流必定会发生重绘,重绘不一定会引发回流。回流所需的成本比重绘高的多,改变父节点里的子节点很可能会导致父节点的一系列回流。
  • 优化方案:
  1. 使用 transform 替代 top
  2. 使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局)
  3. 不要把节点的属性值放在一个循环里当成循环里的变量
  4. 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局
  5. 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用requestAnimationFrame
  6. CSS 选择符从右往左匹配查找,避免节点层级过多
  7. 将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点。比如对于 video 标签来说,浏览器会自动将该节点变为图层。

for in 和for of的区别

for in

1 适合遍历对象 
2 遍历的是key
3 遍历可枚举的所有属性(包括原型上的)
4 可以正确响应break continue

遍历对象:

Object.prototype.method = function(){  
}

var myObject={  
    a:1,  
    b:2,  
    c:3
}

for(var key in myObject) {
    console.log(key);
    //a
    //b
    //c
    //method
}

for in 可以遍历到myObject的原型方法method,如果不想遍历原型方法和属性的话,可以在循环内部判断一下,hasOwnPropery方法可以判断某属性是否是该对象的实例属性

首先输出的是对象的属性名,再是对象原型中的属性和方法, 如果不想让其输出原型中的属性和方法,可以使用hasOwnProperty方法进行过滤
Object.prototype.method = function(){  
}

var myObject={  
    a:1,  
    b:2,  
    c:3
}

for (var key in myObject) {
    if(myObject.hasOwnProperty(key)){
        console.log(key); // a b c
    }
}

遍历数组:

Array.prototype.sayHello = function(){
}
Array.prototype.str = 'world';
var myArray = [1,2,10,30,100];
myArray.name='数组';

for(let index in myArray){
    console.log(index);
    //   0,1,2,3,4,name,str,sayHello
}

注意: for in更适合遍历对象,不要使用for in遍历数组。使用for in 也可以遍历数组,但是会存在以下问题:

  1. index索引为字符串型数字,不能直接进行几何运算

  2. 使用for in会遍历数组所有的可枚举属性,包括原型。例如上栗的原型方法method和name属性

  3. 遍历顺序有可能不是按照实际数组的内部顺序

  4. for in遍历的是数组的索引(即键名)

for of

适合遍历拥有迭代器对象的集合 
遍历的value 
不包括原型上的 
也可以正确响应break continue

遍历对象:

Object.prototype.method = function(){  
}

var myObject={  
    a:1,  
    b:2,  
    c:3
}

for (var key of myObject) {
    console.log(key); //  TypeError: myObject is not iterable
}

遍历数组:

Array.prototype.sayHello = function(){
    console.log("Hello");
}
var myArray = [1,200,3,400,100];

for(let key of myArray){
    console.log(key);
    // 1  200  2  400 400
}

注意:for in遍历的是数组的索引(即键名),而for of遍历的是数组元素值。 所以for in更适合遍历对象,不要使用for in遍历数组。

nodejs模块加载机制


判断两个变量是否相等

在js中比较两个值时,你可能会用相等运算符==或者严格相等运算符 === 

==表示抽象相等,两边值类型不同的时候,会先做隐式类型转换,再对值进行比较;
===表示严格相等,不会做类型转换,两边的类型不同一定不相等。但是===情况下NaN不等于NaN,+0等于-0

console.log(NaN === NaN) //false
console.log(+0 === -0 ) // true

ES6中的Object.is()接受两个参数,并且会在二者的值相等时返回true,此时要求二者的数据类型相同并且值也相等。

console.log( Object.is(NaN, NaN)) //true

console.log(Object.is(+0, -0)) //false

原理:

if (!Object.is) {
    Object.is = function(x, y) {
        if (x === y) { // Steps 1-5, 7-10
            // 针对 +0不等于-0
            return x !== 0 || 1 / x === 1 / y;
        } else {
            // 针对 NaN等于NaN
            return x !== x && y !== y;
        }
    };
}

深拷贝与浅拷贝(前端基础面试高频面试题)

参考链接 https://juejin.cn/post/6844903493925371917#heading-2

引子:

Js里有两种数据类型,基本数据类型和引用数据类型。深拷贝、浅拷贝一般都是针对引用数据类型的。

基本数据类型主要是:undefined,boolean,number,string,null

var a = 1;
var b = a;
a = 2;
console.log(a); // 2
console.log(b); // 1
对于基本数据类型赋值操作,b 复制了 a 的值,而不是引用。即保存 b 的值与 a 的值的内存空间是完全独立的。
var arr1 = [1,2,3,4];
var arr2 = arr1;

arr1.push(5);
console.log(arr1); // [1,2,3,4,5]
console.log(arr2); // [1,2,3,4,5]

arr2.push(6);
console.log(arr1); // [1,2,3,4,5,6]
console.log(arr2); // [1,2,3,4,5,6]

然而,对于引用数据类型的赋值操作,arr2 仅仅是复制了 arr1的引用(也可以称之为指向 arr1 内存地址的指针)。简单来说,就是 arr1 与 arr2 指向了同一个内存空间

浅拷贝:

如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。

function shallowCopy(copyTarget) {
    var obj = {};
    for (var key in copyTarget) {
        obj[key] = copyTarget[key];
    }
    return obj;
}

var json1 = {
    'name': '张三',
    'family': {
        'children': '张三三',
        'wife': '李四'
    }
}

var json2 = shallowCopy(json1);

// before
console.log(json2);

// after
json1.family['father'] = '张一'
console.log(json1);
console.log(json2);


由此可以看出,浅拷贝仅仅拷贝了基本类型的数据,对于引用类型数据,则指向被复制的内存地址,若原地址中的对象发生改变,那么浅复制出来的对象也会相应改变。

深拷贝:

深拷贝可概括为:为引用类型数据成员另辟了一个独立的内存空间,实现真正内容上的拷贝。

function deepCopy(copyTarget) {
    var obj = {};
    for(var key in copyTarget) {
        // 先判断obj[key]是否为对象
        if(typeof copyTarget[key] === "object"){
            // 递归
            obj[key] = deepCopy(copyTarget[key]);
        } else {
            // 如果不是对象,直接赋值即可
            obj[key] = copyTarget[key];
        }
    }
    return obj;
}

var json1 = {
    'name': '张三',
    'family': {
        'children': '张三三','wife': '李四'
    }
}

var json2 = deepCopy(json1);

// before
console.log(json2);

// after
json1.family['father'] = '张一'
console.log(json1);
console.log(json2);


拓展

深复制可以用JSON的方式:JSON.parse(JSON.stringify(obj))

但是JSON复制会忽略掉值为undefined以及函数表达式。

var obj = {
    a: 1,
    b: 2,
    c: undefined,
    sum: function() { return a + b; }
};

var obj2 = JSON.parse(JSON.stringify(obj));
console.log(obj2); //Object {a: 1, b: 2}

前端模块化AMD、CMD、CommonJS&ES6

参考链接 juejin.cn/post/684490…

CommonJS规范:

CommonJS 是服务器端模块的规范, Node.js 采⽤了这个规范。 CommonJS 规范加载模 块是同步的,也就是说,只有加载完成,才能执⾏后⾯的操作。 CommonJS 的规范中通过对 module.exports 或 exports 的属性赋值来达到暴露模块对象的⽬的。CommonJS 是同步加载模块,在浏览器中会出现堵塞情况,所以不适⽤

AMD:

AMD 规范则是异步加载模块,需要定义回调 define ⽅式,AMD推崇依赖前置,--requireJS 推广过程中出现的规范。

define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) { 
    // 等于在最前面声明并初始化了要用到的所有模块
    if (false) {
      // 即便没用到某个模块 b,但 b 还是提前执行了
      b.foo()
    } 
});

CMD

CMD: CMD和AMD类似 不同的是AMD是推崇依赖前置,--requireJS 推广过程中出现的规范。CMD推崇就近依赖。 --sea.js推广过程中出现的规范。 但是因为在AMD&CMD都是在浏览器端使用,采用的是异步加载,其实CMD还是需要在一开始就请求需要的,只是写法上更方便了。(采用的是正则匹配来获得js文件名,所以注释掉的仍然会请求,并且只可以书写字符串,不可以使用表达式)

/** AMD写法 **/
define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) { 
     // 等于在最前面声明并初始化了要用到的所有模块
    a.doSomething();
    if (false) {
        // 即便没用到某个模块 b,但 b 还是提前执行了
        b.doSomething()
    } 
});

/** CMD写法 **/
define(function(require, exports, module) {
    var a = require('./a'); //在需要时申明
    a.doSomething();
    if (false) {
        var b = require('./b');
        b.doSomething();
    }
});

/** sea.js **/
// 定义模块 math.js
define(function(require, exports, module) {
    var $ = require('jquery.js');
    var add = function(a,b){
        return a+b;
    }
    exports.add = add;
});
// 加载模块
seajs.use(['math.js'], function(math){
    var sum = math.add(1+2);
});

es6

ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,旨在成为浏览器和服务器通用的模块解决方案。其模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。

注:es6模块就是⼀个独⽴的⽂件,该⽂件内部的所有变量,外部⽆法获取。如果你希 望外部能够读取模块内部的某个变量,就必须使⽤ export 关键字输出该变量 es6 还可 以导出类、⽅法,⾃动适⽤严格模式

/** 定义模块 math.js **/
var basicNum = 0;
var add = function (a, b) {
    return a + b;
};
export { basicNum, add };

/** 引用模块 **/
import { basicNum, add } from './math';
function test(ele) {
    ele.textContent = add(99 + basicNum);
}

如上例所示,使用import命令的时候,用户需要知道所要加载的变量名或函数名。其实ES6还提供了export default命令,为模块指定默认输出,对应的import语句不需要使用大括号。这也更趋近于ADM的引用写法。

/** export default **/
//定义输出
export default { basicNum, add };
//引入
import math from './math';
function test(ele) {
    ele.textContent = math.add(99 + math.basicNum);
}

ES6的模块不是对象,import命令会被 JavaScript 引擎静态分析,在编译时就引入模块代码,而不是在代码运行时加载,所以无法实现条件加载。也正因为这个,使得静态分析成为可能。

ES6 模块与 CommonJS 模块的差异

1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。

  • CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
  • ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import有点像 Unix 系统的“符号连接”,原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

  • 运行时加载: CommonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。

  • 编译时加载: ES6 模块不是对象,而是通过 export 命令显式指定输出的代码,import时采用静态命令的形式。即在import时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”。

CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。


事件环(个人认为是比较高级的面试题)

参考链接 juejin.cn/post/684490…

浏览器中 Event Loop:

浏览器中, js引擎线程会循环从 任务队列 中读取事件并且执行, 这种运行机制称作 Event Loop (事件循环).

每个浏览器环境,至多有一个event loop。 一个event loop可以有1个或多个task queue(任务队列) 先执行同步的代码,然后js会跑去消息队列中执行异步的代码,异步完成后,再轮到回调函数,然后是去下个事件循环中执行setTimeout 它从script(整体代码)开始第一次循环。之后全局上下文进入函数调用栈。直到调用栈清空(只剩全局),然后执行所有的micro-task(微任务)。当所有可执行的micro-task(微任务)执行完毕之后。循环再次从macro-task 宏任务开始,找到其中一个任务队列执行完毕,然后再执行所有的micro-task,这样一直循环下去。  

从规范上来讲,setTimeout有一个4ms的最短时间,也就是说不管你设定多少,反正最少都要间隔4ms才运行里面的回调。而Promise的异步没有这个问题。Promise所在的那个异步队列优先级要高一些 Promise是异步的,是指他的then()和catch()方法,Promise本身还是同步的 Promise的任务会在当前事件循环末尾中执行,而setTimeout中的任务是在下一次事件循环执行

总结:

  • 我们的同步任务在主线程上运行会形成一个执行栈
  • 如果碰到异步任务,比如setTimeout、onClick等等的一些操作,我们会将他的执行结果放入队列,此期间主线程不阻塞
  • 等到主线程中的所有同步任务执行完毕,就会通过event loop在队列里面从头开始取,在执行栈中执行
  • event loop永远不会断
  • 以上的这一整个流程就是Event Loop(事件循环机制)

  • 补充:

    宏任务:js同步执行的代码块,setTimeout、setInterval、XMLHttprequest等。

    微任务:promise、process.nextTick(node环境)等。
    执行栈中执行的任务都是宏任务,当宏任务遇到Promise的时候会创建微任务,当Promise状态fullfill的时候塞入微任务队列。在一次宏任务完成后,会检查微任务队列有没有需要执行的任务,有的话按顺序执行微任务队列中所有的任务。之后再开始执行下一次宏任务。具体步骤:
    1. 执行主代码块
    2. 若遇到Promise,把then之后的内容放进微任务队列
    3. 一次宏任务执行完成,检查微任务队列有无任务
    4. 有的话执行所有微任务
    5. 执行完毕后,开始下一次宏任务。

    setTimeout(function(){
        console.log(4)
    },0);
    new Promise(function(resolve){
        console.log(1)
        for( var i=0 ; i<10000 ; i++ ){
            i===9999 && resolve()
        }
        console.log(2)
    }).then(function(){
        console.log(5)
    });
    console.log(3);
    //依次输出 1  2  3  5  4
    
    setTimeout(() => {
        console.log(1);
    }, 0);
    
    new Promise((resolve) => {
        console.log(2);
        resolve();
    }).then(() => {
        console.log(3);
    });
    
    console.log(4);
    // 输出最后的结果 2  4  3  1

    分析:

  • setTimeout丢给浏览器的异步线程处理,因为时间是0,马上放入消息队列
  • new Promise里面的console.log(2)加入执行栈,并执行,然后退出
  • 直接resolve,then后面的内容加入微任务队列
  • console.log(4)加入执行栈,执行完成后退出
  • 检查微任务队列,发现有任务,执行console.log(3)
  • 发现消息队列有任务,执行下一次宏任务console.log(1)

  • node环境

    node环境中的事件机制要比浏览器复杂很多,node的事件轮询有阶段的概念。每个阶段切换的时候执行,process.nextTick之类的所有微任务。


    timer阶段

    执行所有的时间已经到达的计时事件

    peding callbacks阶段

    这个阶段将执行所有上一次poll阶段没有执行的I/O操作callback,一般是报错。

    idle.prepare

    可以忽略

    poll阶段

    这个阶段特别复杂

    1. 阻塞等到所有I/O操作,执行所有的callback.
    2. 所有I/O回调执行完,检查是否有到时的timer,有的话回到timer阶段
    3. 没有timer的话,进入check阶段.

    check阶段

    执行setImmediate

    close callbacks阶段

    执行所有close回调事件,例如socket断开。


    函数柯里化

    在一个函数中,首先填充几个参数,然后再返回一个新的函数的技术,称为函数的柯里化。通常可用于在不侵入函数的前提下,为函数 预置通用参数,供多次重复调用。

    const add = function add(x) {
    	return function (y) {
    		return x + y
    	}
    }
    
    const add1 = add(1)
    
    add1(2) === 3
    add1(20) === 21

    HTTP相关

    从输入URL到页面加载发生了什么?

    • 1、浏览器的地址栏输入URL并按下回车。 
    • 2、浏览器查找当前URL是否存在缓存,并比较缓存是否过期。 
    • 3、DNS解析URL对应的IP。 
    • 4、根据IP建立TCP连接(三次握手)。 
    • 5、HTTP发起请求。 
    • 6、服务器处理请求,浏览器接收HTTP响应。 
    • 7、渲染页面,构建DOM树。 
    • 8、关闭TCP连接(四次挥手)。

    AST 

    抽象语法树 (Abstract Syntax Tree),是将代码逐字母解析成 树状对象 的形式。这是语言之间的转换、代码语法检查,代码风格检查,代码格式化,代码高亮,代码错误提示,代码自动补全等等的基础。例如: 

    function square(n){
    	return n * n
    }

    通过解析转化成的AST如下图:


    babel编译原理: 

    babylon 将 ES6/ES7 代码解析成 AST 

    babel-traverse 对 AST 进行遍历转译,得到新的 AST 

    新 AST 通过 babel-generator 转换成 ES5

    关于vuex

    其实在学习vuex的时候最好要模拟数据体验下最好

    模拟数据:

    const fs = require('fs')
    // Koa 为一个class
    const Koa = require('koa')
    const path = require('path')
    // koa 路由中间件
    const Router = require('koa-router');
    const app = new Koa()
    // 实例化路由
    const router = new Router();
    // 处理post请求,把 koa2 上下文的表单数据解析到
    const bodyParser = require('koa-bodyparser');
    app.use(bodyParser())
    
    router.get('/', async (ctx, next) => {
        ctx.response.body = 'hello koa !'
    })
    // 加前缀
    //router.prefix('/api');
    router.get('/api/news', async (ctx, next) => {
        ctx.response.body = {
            result: "这是一条新闻!!!!"
        }
    })
    
    
    app
        .use(router.routes())
        .use(router.allowedMethods());
    app.listen(4000)

    store/index.js

    import Vue from 'vue'
    import 'es6-promise/auto' //引用依赖
    import Vuex from 'vuex'
    import http from '@/http/index.js'
    Vue.use(Vuex) //使用vuex插件
    
    //state
    export const state = {
        news: ''
    }
    
    //action
    // Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit 提交一个 mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters。
    export const actions = {
        //异步操作都在这里
        async getNews(context, grade) {
            //所有在此的方法第一个参数都是context
            let newsResult = await http.get('/api/news', {})
            context.commit('SET_NEWS', newsResult)
        }
    }
    
    //mutations
    export const mutations = {
        SET_NEWS(state, data) {
            state.news = data.data.result
        }
    }
    export default new Vuex.Store({
        state,
        mutations,
        actions
    })

    调用

    <template>
        <div>{{news}}</div>
    </template>
    
    <script>
        /* 辅助函数 */
        import {
            mapState,
            mapGetters,
            mapMutations,
            mapActions
        } from 'vuex'
    
        export default {
            data() {
                return {
                }
            },
            computed: {
                ...mapState(['news'])
            },
            methods: {
                ...mapActions(['getNews'])
            },
            mounted() {
                this.getNews()
                //或者
                //this.$store.dispatch('getNews')
    
            }
        }
    </script>

    参考链接 https://www.yuque.com/testdog/dev/zuphei

    vue路由传参

    通过Vue传递参数可以分为两种方式: params参数 query参数

    params参数传递方式分两种:(1)通过router-link进行跳转 (2)通过编程导航进行路由跳转

    params传值 


    <div
      v-for="item in list"
      @click="getDescribe(item)"
    >
      {{item}}
    </div>
    data() {
        return {
            list: ['101','102','103']
        }
    }

    对应路的由配置如下:

    {
        path: '/two/:id',
        name: 'two',
        component: () => import('@/components/two.vue'),
        meta: {
            title: 'two'
        }
    }

    push 后面可以是对象,也可以是字符串:

    对象:

    getDescribe(item) {
        this.$router.push({
            path: `/two/${item}`
        })
    }

    字符串:

    getDescribe(item) {
        this.$router.push(`/two/${item}`)
    }

    命名的路由:

    getDescribe(item) {
        this.$router.push({ name: 'two', params: { id: `${item}` }})
    }

    注意: 需要注意的是使用params必须和name属性一起使用,否则要跳转的目标路由页面无法通过params获取到传递过来的参数。params: 相当于 post 请求,请求参数不会体现在地址栏中。 这种方法会出现如下问题: 如果子页面点击【刷新】按钮时,刚才传过来的参数并不会同步。因为参数没有同步到网页地址栏中。但是如果要让他体现在地址栏中,可以在配置路由时,写在path属性中。 如下可以解决刷新的问题:

    {
        path: '/two/:id',
        name: 'two',
        component: () => import('@/components/two.vue'),
        meta: {
            title: 'two'
        }
    }

    读取参数:

    console.log(this.$route.params)


    query传值。 类似get传值

    query: 相当于 get 请求,请求参数会体现在地址栏中。

    this.$router.push({
        path: `/two`,
        query: {
            id:item
        }
    })

    http://localhost:8888/#/two?id=101

    console.log(this.$route.query)

    this.$router 和this.$route有何区别?

    上面提到的编程式导航中用this.$router.push()来改变路由,用this.$route.params来得到参数值

    1.$router为VueRouter实例,想要导航到不同URL,则使用$router.push方法。

    2.$route为当前router跳转对象,里面可以获取name、path、query、params等


    vue中watch&&computed 

    computed特性 

    1.是计算值, 

    2.应用:就是简化tempalte里面{{}}计算和处理props或$emit的传值 

    3.具有缓存性,页面重新渲染值不变化,计算属性会立即返回之前的计算结果,而不必再次执行函数  

    注意:computed的值在getter执行后是会缓存的,只有在它依赖的属性值改变之后,下一次获取computed的值时才会重新调用对应的getter来计算

    watch特性 

    1.是观察的动作, 

    2.应用:监听props,$emit或本组件的值执行异步操作 

    3.无缓存性,页面重新渲染时值不变化也会执行

    注意:watch在每次监听的值变化时,都会执行回调。其实从这一点来看,都是在依赖的值变化之后,去执行回调

    总结:如果一个值依赖多个属性(多对一),用computed肯定是更加方便的。如果一个值变化后会引起一系列操作,或者一个值变化会引起一系列值的变化(一对多),用watch更加方便一些。

    watch的回调里面会传入监听属性的新旧值,通过这两个值可以做一些特定的操作。computed通常就是简单的计算。 watch和computed并没有哪个更底层,watch内部调用的是vm.$watch,它们的共同之处就是每个定义的属性都单独建立了一个Watcher对象

    Vue.js 提供了一个方法 watch,它用于观察Vue实例上的数据变动。

    <template>
      <div>
        <input
          type="text"
          v-model="age"
        >
      </div>
    </template>
    
    <script>
        export default {
            data() {
                return {
                    age:18
                }
            },
            watch: {
                age: (newAge,oldAge) => {
                    console.log('新值为:'+newAge+',旧值为:'+oldAge);
                }
            },
            methods: {
            },
            components: {
            },
            computed: {
            },
            mounted() {        
            }
        }
    

    如何理解Virtual DOM

    一、vdom是什么? 

    vdom是虚拟DOM(Virtual DOM)的简称,指的是用JS模拟的DOM结构,将DOM变化的对比放在JS层来做。换而言之,vdom就是JS对象。

    如下真实DOM

    <ul id="list">
        <li class="item">Item1</li>
        <li class="item">Item2</li>
    </ul>

    映射成虚拟DOM就是这样:

    {
        tag: "ul",
        attrs: {
            id:&emsp;"list"
        },
        children: [
            {
                tag: "li",
                attrs: { className: "item" },
                children: ["Item1"]
            }, {
                tag: "li",
                attrs: { className: "item" },
                children: ["Item2"]
            }
        ]
    } 

    二、为什么要用vdom?

    采用JS对象模拟的方法,将DOM的比对操作放在JS层,减少浏览器不必要的重绘,提高效率。

    当然有人说虚拟DOM并不比真实的DOM快,其实也是有道理的。当一个table中的每一条数据都改变时,显然真实的DOM操作更快,因为虚拟DOM还存在js中diff算法的比对过程。所以,性能优势仅仅适用于大量数据的渲染并且改变的数据只是一小部分的情况。

    虚拟DOM更加优秀的地方在于: 

    1、它打开了函数式的UI编程的大门,即UI = f(data)这种构建UI的方式。  

    2、可以将JS对象渲染到浏览器DOM以外的环境中,也就是支持了跨平台开发,比如ReactNative。

    三、diff算法


    vue 组件之间的通信

    1、父组件向子组件通信

    使用props,父组件可以使用props向子组件传递数据。

    father.vue

    <template>
      <div>
        <Child :name="msg"></Child>
      </div>
    </template>
    
    <script>
        import Child from '@/components/child.vue'
        export default {
            components: {
                Child
            },
            data() {
                return {
                    msg: 'feixuan'
                }
            },
            computed: {
    
            },
            mounted() {
    
            },
            methods: {
            }
        }
    </script>

    child.vue

    <template>
      <div>
        {{name}}
      </div>
    </template>
    
    <script>
        export default {
            props:{
                name:{
                    type:String,
                    default:''
                }
            },
            data() {
                return {
                }
            },
            components: {
    
            },
            computed: {
    
            },
            mounted() {
            },
            methods: {
            }
        }
    </script>

    2、子组件向父组件通信

    方法一: 使用vue事件 父组件向子组件传递事件方法,子组件通过$emit触发事件,回调给父组件。

    father.vue

    <template>
      <div>
        <Child @msgFunc="msgEvent"></Child>
        {{message}}
      </div>
    </template>
    
    <script>
        import Child from '@/components/child.vue'
        export default {
            components: {
                Child
            },
            data() {
                return {
                    message: ''
                }
            },
            methods: {
                msgEvent(msg) {
                    console.log(msg)
                    this.message = msg
                }
            },
            computed: {
    
            },
            mounted() {
    
            }
            
        }
    </script>

    child.vue

    <template>
      <div>
        <button @click="handleClick">点我</button>
      </div>
    </template>
    
    <script>
        export default {
            data() {
                return {
                }
            },
            methods: {
                handleClick() {
                    this.$emit('msgFunc','我是来自子组件的消息');
                }
            },
            components: {
    
            },
            computed: {
    
            },
            mounted() {
            }
            
        }
    </script>

    3、非父子组件通信

    对于两个组件不是父子关系,那么又该如何实现通信呢?在项目规模不大的情况下,完全可以使用中央事件总线 EventBus 的方式。如果你的项目规模是大中型的,那么可以使用vuex状态管理

    EventBus 通过新建一个 Vue 事件 bus 对象,然后通过 bus.$emit 触发事件,bus.$on 监听触发的事件。

    Vue.js异步更新DOM策略及nextTick

    在使用vue.js的时候,有时候因为一些特定的业务场景,不得不去操作DOM,比如这样:

    <template>
      <div>
        <div ref="test">{{test}}</div>
        <button @click="handleClick">点击</button>
      </div>
    </template>
    
    <script>
        export default {
            data() {
                return {
                    test: 'begin'
                }
            },
            methods: {
                handleClick () {
                    this.test = 'end';
                    console.log(this.$refs.test.innerText);//打印“begin”
                }
            },
            components: {
            },
            computed: {
            },
            mounted() {
            }
        }
    </script>
    

    打印的结果是begin,为什么我们明明已经将test设置成了“end”,获取真实DOM节点的innerText却没有得到我们预期中的“end”,而是得到之前的值“begin”呢?

    原因

    Vue.js源码的Watch实现。当某个响应式数据发生变化的时候,它的setter函数会通知闭包中的Dep,Dep则会调用它管理的所有Watch对象。触发Watch对象的update实现。我们来看一下update的实现。

    update () {
        /* istanbul ignore else */
        if (this.lazy) {
            this.dirty = true
        } else if (this.sync) {
            /*同步则执行run直接渲染视图*/
            this.run()
        } else {
            /*异步推送到观察者队列中,下一个tick时调用。*/
            queueWatcher(this)
        }
    }

    Vue的双向数据绑定

    Vue是一个MVVM框架,数据绑定简单来说,就是当数据发生变化时,相应的视图会进行更新,当视图更新时,数据也会跟着变化。

    实现数据绑定的方式大致有以下几种:

    - 1、发布者-订阅者模式(backbone.js)
    - 2、脏值检查(angular.js)
    - 3、数据劫持(vue.js)

    Vue.js则是通过数据劫持以及结合发布者-订阅者来实现的,数据劫持是利用ES5的Object.defineProperty(obj, key, val)来劫持各个属性的的setter以及getter,在数据变动时发布消息给订阅者,从而触发相应的回调来更新视图。


    八、十、十六进制转换

    二进制 → 十进制

    方法:二进制数从低位到高位(即从右往左)计算,第0位的权值是2的0次方,第1位的权值是2的1次方,第2位的权值是2的2次方,依次递增下去,把最后的结果相加的值就是十进制的值了。

    例:将二进制的(101011)B转换为十进制的步骤如下:
    
    1. 第0位 1 x 2^0 = 1;
    
    2. 第1位 1 x 2^1 = 2;
    
    3. 第2位 0 x 2^2 = 0;
    
    4. 第3位 1 x 2^3 = 8;
    
    5. 第4位 0 x 2^4 = 0;
    
    6. 第5位 1 x 2^5 = 32;
    
    7. 读数,把结果值相加,1+2+0+8+0+32=43,即(101011)B=(43)D。
    

    八进制 → 十进制

    方法:八进制数从低位到高位(即从右往左)计算,第0位的权值是8的0次方,第1位的权值是8的1次方,第2位的权值是8的2次方,依次递增下去,把最后的结果相加的值就是十进制的值了。   八进制就是逢8进1,八进制数采用 0~7这八数来表达一个数。

    例:将八进制的(53)O转换为十进制的步骤如下:
    
    1. 第0位 3 x 8^0 = 3;
    
    2. 第1位 5 x 8^1 = 40;
    
    3. 读数,把结果值相加,3+40=43,即(53)O=(43)D。

    十进制 → 二进制

    方法:除2取余法,即每次将整数部分除以2,余数为该位权上的数,而商继续除以2,余数又为上一个位权上的数,这个步骤一直持续下去,直到商为0为止,最后读数时候,从最后一个余数读起,一直到最前面的一个余数。 

    例:将十进制的(43)D转换为二进制的步骤如下:
    
    1. 将商43除以2,商21余数为1;
    
    2. 将商21除以2,商10余数为1;
    
    3. 将商10除以2,商5余数为0;
    
    4. 将商5除以2,商2余数为1;
    
    5. 将商2除以2,商1余数为0; 
    
    6. 将商1除以2,商0余数为1; 
    
    7. 读数,因为最后一位是经过多次除以2才得到的,因此它是最高位,读数字从最后的余数向前读,101011,即(43)D=(101011)B。


    函数节流跟防抖

    函数防抖(debounce)

    防抖函数 debounce 指的是某个函数在某段时间内,无论触发了多少次回调,都只执行最后一次。假如我们设置了一个等待时间 3 秒的函数,在这 3 秒内如果遇到函数调用请求就重新计时 3 秒,直至新的 3 秒内没有函数调用请求,此时执行函数,不然就以此类推重新计时。

    应用场景:

    用户需要不断的输入值,比如填写input表单验证或者在搜索框要输入一些内容。 

    用户不断地进行tab切换页面操作时。

    原理及实现 :

    实现原理就是利用定时器,函数第一次执行时设定一个定时器,之后调用时发现已经设定过定时器就清空之前的定时器,并重新设定一个新的定时器,如果存在没有被清空的定时器,当定时器计时结束后触发函数执行。

    function  debounce(fn,delay) {
        let timer;
        return function () {
            var context = this; //保存当前this指向当前对象
            var args = arguments;  //获取隐式传入的参数
            clearTimeout(timer);
            timer = setTimeout(function () {
                fn.apply(context, args);
            }, delay);
        }
    }

    函数节流(throttle)

    规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效,
    即每隔一段时间后执行一次,也就是降低频率,将高频操作优化成低频操作
    

    应用场景

    滚动上拉加载,滚动下拉刷新。 oversize,mousemove ,mousehover。 还有拖拽事件等等。

    function  throttle(fn,delay) {
        let timer = null;
        return function () {
            var context = this; //保存当前this指向当前对象
            var args = arguments;  //获取隐式传入的参数
            if(timer == null){
                timer = setTimeout(() => {
                    fn.apply(context,args)
                }, delay);
            }
        }
    }

    总结 :

    • 防抖 是将多次执⾏变为最后⼀次执⾏
    • 节流 是将多次执⾏变成每隔⼀段时间执⾏



    斐波那契数列

    斐波那契数列也叫黄金分割数列,也叫兔子数列 

    原理:假定一对大兔子每月能生一对小兔子,且每对新生的小兔子经过一个月可以长成一对大兔子,如果不发生死亡,且每次均生下一雌一雄,问一年后共有多少对兔子?

    月份兔子情况总数
    第0个月a(小兔子)1
    第1个月a(具备繁殖能力)1
    第2个月b(生啦生啦)+ a(他父母)2
    第3个月b(2月份出生的具备繁殖能力,正跃跃欲试) + b2(他父母又生二胎了) +a(他父母)3
    第4个月c(2月份的兔子b喜当爹)+b(二月份出生的兔子) + b2(二胎具备繁殖能力,准备生娃) +a(他父母)+d(a生三胎)5

    1、1 、2、3、5、8、13、21、34、55、89…… 

    所以规律就是 fn(n)=fn(n-1)+fn(n-2)

    迭代 方式

    /*
    *i 月份
    */
    function fn(i){
        var a=[];
        /*0个月什么都不存在*/
        a[0]=0;
        a[1]=1;
        for(var j = 2; j<= i;j++){
            a[j]=a[j-1] + a[j-2];
        }
        return a[i]
    }

    递归方式

    /*
    * i 月份
    */
    function fn(i){
        if(i < 2){return i === 0 ? 0 : 1;}
        return fn(i-1)+fn(i-2)
    }

    总结: 针对这个例子来说,这里的递归会进行太多次的调用(比迭代多),所以简洁的背后牺牲的是性能


    vue-router实现原理

    这个是阿里的一道面试题

    核心原理: 更新视图但不重新请求页面。

    vue-router实现单页面路由跳转,提供了三种方式:hash模式、history模式、abstract模式,根据mode参数来决定采用哪一种方式。

    路由模式

    vue-router 提供了三种运行模式: 

    hash: 使用 URL hash 值来作路由。默认模式。 

    history: 依赖 HTML5 History API 和服务器配置。查看 HTML5 History 模式。 

    abstract: 支持所有 JavaScript 运行环境,如 Node.js 服务器端

    Hash模式:

    hash即浏览器url中#后面的内容,包含#。hash是URL中的锚点,代表的是网页中的一个位置,单单改变#后的部分,浏览器只会加载相应位置的内容,不会重新加载页面。 也就是说 即#是用来指导浏览器动作的,对服务器端完全无用,HTTP请求中,不包含#。 每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用”后退”按钮,就可以回到上一个位置。 所以说Hash模式通过锚点值的改变,根据不同的值,渲染指定DOM位置的不同数据。

    History模式:

    HTML5 History API提供了一种功能,能让开发人员在不刷新整个页面的情况下修改站点的URL,就是利用 history.pushState API 来完成 URL 跳转而无须重新加载页面; 由于hash模式会在url中自带#,如果不想要很丑的 hash,我们可以用路由的 history 模式,只需要在配置路由规则时,加入"mode: 'history'",这种模式充分利用 history.pushState API 来完成 URL 跳转而无须重新加载页面。 有时,history模式下也会出问题: eg: hash模式下:xxx.com/#/id=5 请求地址为 xxx.com,没有问题。 history模式下:xxx.com/id=5 请求地址为 xxx.com/id=5,如果后端没有对应的路由处理,就会返回404错误; 为了应对这种情况,需要后台配置支持: 在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。

    abstract模式:

    abstract模式是使用一个不依赖于浏览器的浏览历史虚拟管理后端。 根据平台差异可以看出,在 Weex 环境中只支持使用 abstract 模式。 不过,vue-router 自身会对环境做校验,如果发现没有浏览器的 API,vue-router 会自动强制进入 abstract 模式,所以 在使用 vue-router 时只要不写 mode 配置即可,默认会在浏览器环境中使用 hash 模式,在移动端原生环境中使用 abstract 模式。 (当然,你也可以明确指定在所有情况下都使用 abstract 模式)。


    观察者模式

    异步编程

    async 及 await

    async 是让方法变成异步。 await 是等待异步方法执行完成。

    注意:await 必须在 async 方法中才可以使用因为await 访问本身就会造成程序停止堵塞,所以必须在异步方法中才可以使用

    使用await语法的时候,后面的函数需要返回Promise对象

    async test1() {
        let result = await this.$http.get('/api/job',{})
        console.log(result)
    }

    十大经典排序算法

    冒泡排序:

    两两元素进行比较,较大者放置后头,类似水中的气泡较大者浮在水的上端 

    参考:juejin.cn/post/684490…

    function bubble(arr) {
      for(var i = 0; i < arr.length; i++) {
        for(var j = 0; j < arr.length - 1- i; j++) {
          if(arr[j] > arr[j + 1]) {
            var max = arr[j];
            arr[j] = arr[j+1];//交换数据
            arr[j+1] = max;//交换数据
          }
        }
      }
      console.log(arr)
    }
    
    bubble([1,4,6,2,7,2])// [1, 2, 2, 4, 6, 7]

    快速排序:

    取一个元素为基准,把序列分成两部分,小于基准的放到它的左面,大于等于的放到它的右面,然后在把左面和右面的子序列再进行上述的拆分,直到子序列不可再分割(小于2个元素),最终达到整个序列有序 

    paste image


    function quickSort(arr) {
      if(arr.length < 2) {
        return arr;
      } else {
        const pivot = arr[0]; // 基准值
        const pivotArr = []; // 一样大的放中间
        const lowArr= []; // 小的放左边
        const hightArr = []; // 大的放右边
        arr.forEach(current => {
          if(current === pivot) pivotArr.push(current);
          else if(current > pivot) hightArr.push(current);
          else lowArr.push(current);
        })
        return quickSort(lowArr).concat(pivotArr).concat(quickSort(hightArr));
      }
    }
    
    console.log(quickSort([4,6,2,3,1,5,7,8])) // [1, 2, 3, 4, 5, 6, 7, 8]

    简单选择排序:

    首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。 重复第二步,直到所有元素均排序完毕。

    const selectionSort = array => {
      const len = array.length;
      let minIndex, temp;
      for (let i = 0; i < len - 1; i++) {
        minIndex = i;
        for (let j = i + 1; j < len; j++) {
          if (array[j] < array[minIndex]) {
            // 寻找最小的数
            minIndex = j; // 将最小数的索引保存
          }
        }
        temp = array[i];
        array[i] = array[minIndex];
        array[minIndex] = temp;
        console.log('array: ', array);
      }
      return array;
    };
    selectionSort([6,4,3,8])//[3, 4, 6, 8]


    async和defer

    异步加载js有三种 : defer 、 async 、 动态创建script标签 、 按需异步载入js

    async : 并行加载脚本文件,下载完毕立即解释执行代码,不会按照页面上的script顺序执行。 defer : 并行下载js,会按照页面上的script标签的顺序执行,然后在文档解析完成之后执行脚本

    defer 与 async 的相同点是采用并行下载,在下载过程中不会产生阻塞。区别在于执行时机,async 是加载完成后自动执行,而 defer 需要等待页面完成后执行。

    解析:

    <script src="script.js"></script>

    没有 defer 或 async,浏览器会立即加载并执行指定的脚本,“立即”指的是在渲染该 script 标签之下的文档元素之前,也就是说不等待后续载入的文档元素,读到就加载并执行。

    <script async src="script.js"></script>

    有 async,加载和渲染后续文档元素的过程将和 script.js 的加载与执行并行进行(异步)。

    <script defer src="myscript.js"></script>

    有 defer,加载后续文档元素的过程将和 script.js 的加载并行进行(异步),但是 script.js 的执行要在所有元素解析完成之后,DOMContentLoaded 事件触发之前完成。

    Load 事件触发代表页面中的 DOM,CSS,JS,图片已经全部加载完毕。DOMContentLoaded 事件触发代表初始的 HTML 被完全加载和解析,不需要等待 CSS,JS,图片加载。

    call、apply、bind方法的使用

    call 和 apply 是为了动态改变 this 而出现的,当一个 object 没有某个方法,但是其他的有,我们可以借助 call 或 apply 用其它对象的方法来操作。

    知乎简单易懂答案:
    猫吃鱼狗吃肉,奥特曼打怪兽
    有一天,狗想吃鱼了
    猫.吃鱼.call(狗,鱼)
    狗就吃到鱼了
    猫成精了,想打怪兽
    奥特曼.打怪兽.call(猫,小怪兽)
    
    obj.call(thisObj, arg1, arg2, ...)
    obj.apply(thisObj, [arg1, arg2, ...])
    
    两者作用一致,都是把obj(即this)绑定到thisObj,这时候thisObj具备了obj的属性和方法。
    或者说thisObj继承了obj的属性和方法。唯一区别是apply接受的是数组参数,call接受的是连续参数。

    例子:

    var obj1 = {
      value: 1
    }
    
    function say() {
      console.log(this.value)
    }
    
    say() // 输出undefined
    say.call(obj1) // 输出1

    注意两点 : call 改变了 this 的指向,此时的 this 指到了 obj1 say 函数执行了

    bind方法的特殊性:

    之所以把bind方法单独放出来是因为bind方法和前面两者还是有不小的区别的,虽然都是动态改变this的值,举个例子

    var obj = {
        x: 81,
    };
    
    var foo = {
        getX: function() {
            return this.x;
        }
    }
    console.log(foo.getX.bind(obj)());  //81
    console.log(foo.getX.call(obj));    //81
    console.log(foo.getX.apply(obj));   //81

    有没有注意到使用bind方法时候后面还要多加上一对括号,因为使用bind只是返回了对应函数并没有立即执行,而call和apply方法是立即执行的

    bind与call和apply又有区别,一个函数被call的时候,会直接去调用,但是bind是会返回一个函数,当这个函数执行的时候,bind()的第一个参数将作为它运行时的this。

    手动实现bind方法

     参考链接 segmentfault.com/a/119000001…

    bind方法的全称是Function.prototype.bind()  

    我们先来看看bind方法的定义:

    bind方法会创建一个新函数。当这个新函数被调用时,bind的第一个参数将作为它运行时的this(该参数不能被重写), 之后的一序列参数将会在传递的实参前传入作为它的参数。
    新函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器,提供的this值被忽略。

    做了什么:

    1. 返回了一个新函数
    2. 将this(传入的参数)关键字绑定到该函数 参数合并,
    3. 将bind函数的参数与原来的函数参数合并作为参数传给创建的新的函数
    4. 返回该函数

    初步思路

    1. 因为bind方法不是立即执行函数,需要返回一个待执行的函数,这里可以利用闭包:return function(){}
    2. 作用域绑定:可以使用applycall方法来实现;
    3. 参数传递:由于参数的不确定性,需要用apply传递数组;
    Function.prototype.mybind = function(context){
        //只能对函数执行bind方法
        if (typeof this !== "function") {
            throw new TypeError(this + ' must be a function');
        }
        let self = this;
        /**
         * 由于参数的不确定性,我们用 arguments 来处理
         * 这里的 arguments 只是一个类数组对象,可以用数组的 slice 方法转化成标准格式数组
         * 除了作用域对象 self 以外,后面的所有参数都需要作为数组进行参数传递
         * 获取除第一个以外的参数,第一个参数是上下文,其它才是真正的参数。
         */
        var args = Array.prototype.slice.call(arguments, 1);
        // 利用一个空函数作为中转
        let tempFn = function() {};
        var resultFn = function () {
            // 将新函数执行时的参数 arguments 全部数组化,然后与绑定时传参 arg 合并
            var innerArgs = Array.prototype.slice.call(arguments);
            // 如果 返回函数被当做构造函数后,生成的对象是 tempFn 的实例,此时应该将 this 的指向指向创建的实例。
            // 如果是new操作符实例出来对象,则为this。
            // 如果是直接执行方法,则为context
            if (this instanceof tempFn) {
                return self.apply(this, args.concat(innerArgs));
            } else {
                return self.apply(context, args.concat(innerArgs))
            }
        }
        // 中转原型链
        // 让返回函数拥有bind函数的原型。
        // 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值
        tempFn.prototype = self.prototype;
        resultFn.prototype = new tempFn();
        return resultFn;
    
    }
    
    var testFn = function(obj, arg) {
        console.log('作用域对象属性值:' + this.value);
        console.log('绑定函数时参数对象属性值:' + obj.value);
        console.log('调用新函数参数值:' + arg);
    }
    var testObj = {
        value: 1
    };
    var newFn = testFn.mybind(testObj, {value: 2});
    newFn('hello world');
    
    // 作用域对象属性值:1
    // 绑定函数时参数对象属性值:2
    // 调用新函数参数值:hello world


    请说出以下打印结果

    let a = {a: 10};
    let b = {b: 10};
    let obj = {
        a: 10
    };
    obj[b] = 20;
    obj["c"] = 20;
    console.log(obj[a]);
    console.log(obj)
    // 20
    //{a: 10, [object Object]: 20, c: 20}
    这道题目主要考对JS数据类型的熟练度以及对ES6中属性名表达式的理解。在上题中obj[b] = 20的赋值操作后,obj其实已经变成了{a: 10, [object Object]: 20},这是因为如果属性名表达式是一个对象的话,那么默认情况下会自动将对象转为字符串[object Object],最后一步获取obj[a]时,a本身也是一个对象,所以会被转换为获取obj['[object Object]']也就是上一步赋值的20


    Vue 的生命周期

    beforeCreate:

    vue实例的挂载元素el和数据对象data都为undefined,还未初始化

    created:

    实例创建完成 vue实例的数据对象data有了,el还没有。并已经完成以下配置:数据观测(data observer),属性和方法的运算, watch/event 事件回调
    此时可以调用methods中定义的方法,修改data的数据,并且可触发响应式变化、computed值重新计算,watch到变更等

    还未挂载到DOM,不能访问到$el属性,$ref属性内容为空数组

    beforeMount: 

    vue 实例的$el 和 data 都初始化了,但还是挂载之前为虚拟的 dom 节点,data.message 还未替换

    mounted:

    实例挂载到DOM上,此时可以通过DOM API获取到DOM节点,$ref属性可以访问,data.message 成功渲染

    beforeUpdate:  

    这里的更新对象是模板,即需要虚拟 DOM 重新渲染和打补丁,beforeUpdate发生在以上两个流程之前,此时新的虚拟DOM已经生成

    如果发生变更的数据在模板中并没有使用(包括直接和间接,间接:比如某个依赖该数据的计算属性在模板中使用了),则不会触发更新流程!!!

    updated: 

    由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。

    当这个钩子被调用时,组件 DOM 已经更新,可以执行依赖于 DOM 的操作

    beforeDestroy: 

    实例销毁之前调用。在这一步,实例仍然完全可用,this仍能获取到实例

    一般在这一步中进行:销毁定时器、解绑全局事件、销毁插件对象等操作

    destroyed:

    Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁


    闭包

    参考 https://segmentfault.com/a/1190000000652891

    闭包就是能够读取其他函数内部变量的函数。由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。

    要理解闭包,首先必须理解Javascript特殊的变量作用域。

    变量的作用域无非就是两种:全局变量和局部变量。

    Javascript语言的特殊之处,就在于函数内部可以直接读取全局变量。

      var n = 999;
      function f() {
         console.log(n)
       }
     f(); // 999     
    
    

    另一方面,在函数外部自然无法读取函数内的局部变量。

    function f() {
       var n= 999;
    }
    console(n); //Uncaught ReferenceError: n is not defined
    
    

    这里有一个地方需要注意,函数内部声明变量的时候,一定要使用var命令。如果不用的话,你实际上声明了一个全局变量!

    function f() {
        n = 999;
    }
    f();
    console.log(n); // 999

    出于种种原因,我们有时候需要得到函数内的局部变量。但是,前面已经说过了,正常情况下,这是办不到的,只有通过变通方法才能实现。

    那就是在函数的内部,再定义一个函数。

    function f1() {
        var n = 999;
        function f2(){
             console.log(n); // 999
        }
    }

    在上面的代码中,函数f2就被包括在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。但是反过来就不行,f2内部的局部变量,对f1 就是不可见的。这就是Javascript语言特有的“链式作用域”结构(chain scope),

    子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。

    既然f2可以读取f1中的局部变量,那么只要把f2作为返回值,我们不就可以在f1外部读取它的内部变量了吗!

    function f1() {
       n = 999;
         function f2(){
              console.log(n);
         }
       return f2;
    }
    var result=f1();
    result();// 999

    闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中

    function f1() {
        var n = 999;
        nAdd = function() {
            n += 1
         }
         function f2() {
           console.log(n);
         }
         return f2;
    
     }
     var result = f1();
     result(); // 999
     nAdd();
     result(); // 1000

    在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。

    为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。

    这段代码中另一个值得注意的地方,就是“nAdd=function(){n+=1}”这一行,首先在nAdd前面没有使用var关键字,因此 nAdd是一个全局变量,而不是局部变量。其次,nAdd的值是一个匿名函数(anonymous function),而这个

    匿名函数本身也是一个闭包,所以nAdd相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。

    总结

    特性: 

    1.函数嵌套函数 

    2.函数内部可以引用外部的参数和变量 

    3.参数和变量不会被垃圾回收机制回收

    闭包的缺点就是常驻内存,会增大内存使用量,使用不当很容易造成内存泄露。

    为什么要使用闭包:

    为了设计私有方法和变量,避免全局变量污染 希望一个变量长期驻扎在内存中

    数组去重

    利用数组的 indexOf 属性

    let arr = [5,7,8,8]
    console.log(arr.indexOf(5))//0
    console.log(arr.indexOf(8))//2
    console.log(arr.indexOf(9))//-1
    
    function unique(origin) {
        var result = [];
        for (var i = 0; i < origin.length; i++) {
            var item = origin[i];
            if (result.indexOf(item) === -1) {
                result.push(item);
            }
        }
        return result;
    }
    console.log(unique(arr))//[5, 7, 8]

    数组的 filter 属性和 indexOf 属性

    let arr = [5,7,8,8]
    function unique(origin) {
      var result = origin.filter(function(item, index, array) {
        // 获取元素在源数组的位置,只返回那些索引等于当前元素索引的值。
        return array.indexOf(item) === index;
      });
      return result;
    }
    console.log(unique(arr))//[5, 7, 8]

    利用 ES6 Set

    ES6 提供了新的数据结构 Set,它类似于数组,但是成员的值都是唯一的,没有重复的值。向 Set 加入值的时候,不会发生类型转变,所以 5 和 ‘5’ 是两个不同的值。Set 内部判断两个值是否相同,用的是类似于 “===”的算法,但是区别是,在 set 内部认为 NaN 等于 NaN 

    let arr = [5,7,8,8]
    function unique(origin) {
      return Array.from(new Set(origin));
    }
    console.log(unique(arr))//[5, 7, 8]


    盒子模型

    首先盒子模型是由content、padding、border、margin 4部分组成

    Box-Model

    分类: 标准盒子模型 和 ie盒子模型

    区别:

    • 在 标准盒子模型中,width 和 height 指的是内容区域的宽度和高度。增加内边距、边框和外边距不会影响内容区域的尺寸,但是会增加元素框的总尺寸。
    • IE盒子模型中,width 和 height 指的是内容区域+边框+内边距的宽度和高度。

    但是目前已经统一去起来了

    <!DOCTYPE html>

    CSS3提供了可以切换盒子模型模式的属性:box-sizing,它有2个值,分别是content-box和border-box

    content-box:

    让元素维持W3C的标准盒模型。元素的宽度/高度由border + padding + content的宽度/高度决定,设置width/height属性指的是content部分的宽/高 

    border-box:

    让元素维持IE传统盒模型(IE6以下版本和IE6~7的怪异模式)。设置width/height属性指的是border + padding + content 


    在浏览器输入 URL 回车之后发生了什么

    参考 ruoduan.top/2019/04/11/…

    当我们在web浏览器的地址栏中输入: www.baidu.com,然后回车,到底发生了什么

    1.对www.baidu.com这个网址进行DNS域名解析,得到对应的IP地址   

    2.根据这个IP,找到对应的服务器,发起TCP的三次握手   

    3.建立TCP连接后发起HTTP请求   

    4.服务器响应HTTP请求,浏览器得到html代码   

    5.浏览器解析html代码,并请求html代码中的资源(如js、css图片等)(先得到html代码,才能去找这些资源)   

    6.浏览器对页面进行渲染呈现给用户

    三个主要过程:

    • DNS 解析
    • TCP 连接
    • HTTP 请求/响应

    最后一步对前端很重要 浏览器是如何对页面进行渲染的?

    a: 解析html文件构成 DOM树, 

    b: 解析CSS文件构成渲染树, 

    c: 边解析,边渲染 ,  

    d: JS 单线程运行,JS有可能修改DOM结构,意味着JS执行完成前,后续所有资源的下载是没有必要的,所以JS是单线程,会阻塞后续资源下载

    new操作符具体做了什么

    function Animal(name) {
        this.name = name;
    }
    Animal.prototype.run = function() {
        console.log(this.name + 'can run...');
    }
    
    var cat = new Animal('cat');
    cat.run() //catcan run...
    console.log(cat.__proto__ === Animal.prototype); // true

    new共经历了四个过程。

    var fn = function () { };
    var fnObj = new fn();

    1、创建了一个空对象

    var obj = new object();

    2、设置原型链

    obj._proto_ = fn.prototype;

    3、让fn的this指向obj,并执行fn的函数体

    var result = fn.call(obj);

    4、判断fn的返回值类型,如果是值类型,返回obj。如果是引用类型,就返回这个引用类型的对象。

    if (typeof(result) == "object"){  
        fnObj = result;  
    } else {  
        fnObj = obj;
    }  


    Symbol,Map和Set

    参考 blog.csdn.net/xzz2222/art…

    为啥需要Symbol:

    一个新规则的提出,必然是因为有需求,熟悉ES5的人都知道,ES5里面对象的属性名都是字符串,如果你需要使用一个别人提供的对象,你对这个对象有哪些属性也不是很清楚,但又想为这个对象新增一些属性,那么你新增的属性名就很可能和原来的属性名发送冲突,显然我们是不希望这种情况发生的。所以,我们需要确保每个属性名都是独一无二的,这样就可以防止属性名的冲突了。因此,ES6里就引入了Symbol,用它来产生一个独一无二的值。

    Symbol是什么:

    Symbol实际上是ES6引入的一种原始数据类型,除了Symbol,JavaScript还有其他5种数据类型,分别是Undefined、Null、Boolean、String、Number,这5种数据类型都是ES5中就有的。

    怎么生成一个Symbol类型的值:

    既然我们已经知道了Symbol是一种原始的数据类型,那么怎么生成这种数据类型的值呢?Symbol值是通过Symbol函数生成的,如下:

    let s = Symbol();
    console.log(s);  // Symbol()
    typeof s;  // "symbol"

    上面代码中,s就是一个Symbol类型的值,它是独一无二的。

    Symbol函数前不能用new

    Symbol函数不是一个构造函数,前面不能用new操作符。所以Symbol类型的值也不是一个对象,不能添加任何属性,它只是一个类似于字符型的数据类型。如果强行在Symbol函数前加上new操作符,会报错,如下:

    let s = new Symbol();// Uncaught TypeError: Symbol is not a constructor(…)

    Symbol函数的参数

    1.字符串作为参数

    用上面的方法生成的Symbol值不好进行区分,Symbol函数还可以接受一个字符串参数,来对产生的Symbol值进行描述,方便我们区分不同的Symbol值。

    let s1 = Symbol('s1');
    let s2 = Symbol('s2');
    console.log(s1);  // Symbol(s1)
    console.log(s2);  // Symbol(s2)
    s1 === s2;  //  false
    let s3 = Symbol('s2');
    s2 === s3;  //  false
    

    从上面代码可以看出:

    1. 给Symbol函数加了参数之后,控制台输出的时候可以区分到底是哪一个值;
    2. Symbol函数的参数只是对当前Symbol值的描述,因此相同参数的Symbol函数返回值是不相等的;

    2.对象作为参数

    如果Symbol函数的参数是一个对象,就会调用该对象的toString方法,将其转化为一个字符串,然后才生成一个Symbol值。所以,说到底,Symbol函数的参数只能是字符串。

    set数据集合:类似于数组 

    set:是一个集合,类似于数组,与数组的主要区别是没有重复的元素。主要的作用可以进行去重。 

    重点:一个属性,四个方法 

    1、size属性:返回set数组的长度,类似于lenght 

    2、四个方法:add,delete,clear,has 

    let set =new Set([1,2,3,4,1]);
    console.log(set.size); //4
    //添加元素
    set.add(7);
    console.log(set); // {1, 2, 3, 4, 7}
    //删除数组中的某个元素
    set.delete(3);
    console.log(set); // {1, 2, 4, 7}
    //检测数组中是否含有某个元素,返回值为布尔
    console.log(set.has(2)); //true
    //清空数组
    set.clear();
    console.log(set) //{}

    map数据集合:类似于对象 

    let obj={a:1};
    const map = new Map([
        ['name','java'],
        ['feel','今天天气贼好'],
        ['jieguo','适合看帅哥'],
        [obj,'是!']
    ]);
    console.log(map);//{"name" => "java", "feel" => "今天天气贼好", "jieguo" => "适合看帅哥", {…} => "是!"}

    Map与Object的区别 

     1、Map的size属性可以直接获取键值对个数 

     2、两者都是对应的keys-value键值对, 但是Object的keys只能是字符串或者 Symbols, Map的key可以是任意值,比如数组、对象等  


    总结:Map 对象保存键值对。一个对象的键只能是字符串或者 Symbols,但一个 Map 的键可以是任意值。 Set 对象允许你存储任何类型的唯一值,Set对象是值的集合,Set中的元素只会出现一次 Symbol 是一种特殊的、不可变的数据类型,可以作为对象属性的标识符使用(Symbol([description]) )

    JavaScript中的类

    类的声明:

    使用class关键来声明类

    class Person {
      // 相当于Person构造函数
      constructor (name) {
        this.name = name
      }
      // 相当于Person.prototype.sayName
      sayName () {
        console.log(this.name)
      }
    }
    const person = new Person('AAA')
    person.sayName()                      // AAA
    console.log(person instanceof Person) // true
    console.log(person instanceof Object) // true 

    地将类导出为 ES2015 模块的一部分

    默认语法

    export default class Person {
     // The body of class
    } 

    或者命名导出

    export class Person  {
      // The body of class
    }

    分析:

    1)  constructor():我们可以看到constructor()方法相当于我们上面写到的Person构造函数,在constructor()方法中我们定义了一个name的自有属性。所谓自有属性,就是类实例的属性,其不会出现在原型上,且只能在类的构造函数或方法中被创建

    2)sayName():sayName()方法就相当于我们上面写到的Person.prototype.sayName。有一个特别需要注意的地方就是:与函数有所不同,类属性不可被赋予新值,例如:Person.prototype就是这样一个只读的类属性。

    静态成员 static

    顾名思义是静态方法的意思,类相当于实例的原型, 所有在类中定义的方法, 都会被实例继承。 如果在一个方法前, 加上static关键字, 就表示该方法不会被实例继承, 而是直接通过类来调用, 这就称为“ 静态方法”。静态方法调用直接在类上进行,而在类的实例上不可被调用。静态方法通常用于创建 实用/工具 函数。在ES6中,类语法简化了创建静态成员的过程,在方法或者访问器属性名前面使用正式的静态注释static即可。 

    注意:静态成员只能在类中访问,不能在实例中访问

        class Person {
              constructor (name) {
                this.name = name
              }
              sayName () {
                console.log(this.name)
              }
              static create (name) {
                return new Person(name)
              }
            }
            const person = Person.create('AAA')
            person.sayName() // AAA

    继承extends

           class Person {
                constructor(name) {
                    this.name = name;
                }
                speak() {
                    console.log(this.name)
                }
            }
    
            class Child extends Person {
                constructor(name) {
                    super(name); // 相当于  Person.call(this)
                    this.name = name;
                }
                
            }
            let c1 = new Child("aaa")
            c1.speak(); // aaa

    es5的写法

            function Person(name){
               this.name = name
            }
    
            Person.prototype.speak = function(){
               console.log(this.name);
            }
    
            function Child(name){
              Person.call(this);
              this.name = name;
            }
            Child.prototype = Object.create(Person.prototype);
            Child.prototype.constructor = Child;//因为上一步造成constructor为Animal
    
            //或者可以把上边的两行改成下面这样的写法
            Child.prototype = Object.create(Person.prototype, {
              constructor: Child,
            });

    注意

    super在类的继承中,有两个用法 

    • 作为函数使用,如上边的例子中的super() 
    • super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。

    作为函数使用:

           class Child extends Person {
                constructor(name) {
                    super(name);
                    this.name = name;
                }
                
            }

    作为对象使用:

            class Person {
                constructor(name) {
                    this.name = name;
                }
                speak() {
                    console.log(this.name)
                }
            }
            Person.prototype.name ="bbbb"
            class Child extends Person {
                constructor(name) {
                    super();
                    this.name = name;
                    console.log(super.name) // bbb
                }
                
            }
            let c1 = new Child("aaa")
            c1.speak(); // aaa

    在静态方法中指向父类

            class Person {
                constructor(name) {
                    this.name = name;
                }
                static speak() {
                    console.log(111) 
                }
            }
            class Child extends Person {
                constructor() {
                    super();
                }
                static say(name) {
                    super.speak()
                }
                
            }
           
           let c1 = new Child();
           Child.say(111); // 111

    Vue 数组处理

    <template>
      <div>
        {{list[2]}}
      </div>
    </template>
    <script>
        export default {
            data() {
                return {
                    list: [1,3,5]
                }
            },
            methods: {
            },
            components: {
            },
            computed: {
            },
            mounted() {
                this.list[2] = 8 // 没有效果
                this.$set(this.list, 2, 8); //有效果
            }
        }
    </script>

    对于set这个方法的解释: 

    this.$set(数组或者对象,修改的下标或者对象属性名,修改的值)

    GET,POST,PUT,Delete

    GET请求会向数据库获取信息,只是用来查询数据,不会修改,增加数据。使用URL传递参数,对所发送的数量有限制,一般在2000字符


    POST向服务器发送数据,会改变数据的种类等资源,就像insert操作一样,会创建新的内容,大小一般没有限制,POST安全性高,POST不会被缓存


    PUT请求就像数据库的update操作一样,用来修改数据内容,不会增加数据种类


    Delete用来删除操作

    GET和POST的区别:

    1.GET使用URL或Cookie传参,而POST将数据放在BODY中,这个是因为HTTP协议用法的约定。并非它们的本身区别。
    2.GET方式提交的数据有长度限制,则POST的数据则可以非常大,这个是因为它们使用的操作系统和浏览器设置的不同引起的区别。也不是GET和POST本身的区别。
    3.POST比GET安全,因为数据在地址栏上不可见,这个说法没毛病,但依然不是GET和POST本身的区别。

    4.GET和POST最大的区别主要是GET请求是幂等性的,POST请求不是。(幂等性:对同一URL的多个请求应该返回同样的结果。)因为get请求是幂等的,在网络不好的隧道中会尝试重试。如果用get请求增数据,会有重复操作的风险,而这种重复操作可能会导致副作用


    vue router的钩子函数

    1、全局的钩子

    beforeEach(to,from,next)(全局前置守卫)  页面加载之前  

    • to:即将要进入的目标
    • from:当前导航正要离开的路由
    • next:function函数,必须调用
    应用场景:
    进行页面的跳转,如判断登录页面是否需要进行拦截

    afterEach(to,from,next)(全局后置守卫) 页面加载之后,有两个参数:to/from ,全局后置钩子在所有路由跳转结束的时候调用这些钩子不会接受 next 函数也不会改变导航本身

    vue router.beforeResolve(全局解析守卫)


    注:beforeEach和afterEach都是vue-router实例对象的属性,每次跳转前,beforeEach和afterEach都会执行。

    2、组建内的导航钩子: 

    beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave,直接在路由组件内部进行定义的  

    beforeRouteEnter(to, from, next) :

    在渲染该组件的对应路由被确认前调用,用法和参数与beforeEach类似,next需要被主动调用
    注意:

    • 此时组件实例还未被创建,不能访问this
    • 可以通过传一个回调给 next来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数
    beforeRouteEnter (to, from, next) {
      // 这里还无法访问到组件实例,this === undefined
      next( vm => {
        // 通过 `vm` 访问组件实例
      })
    }
    • 可以在这个守卫中请求服务端获取数据,当成功获取并能进入路由时,调用next并在回调中通过 vm访问组件实例进行赋值等操作
    • beforeRouteEnter触发在导航确认、组件实例创建之前:beforeCreate之前;而next中函数的调用在mounted之后:为了确保能对组件实例的完整访问

    beforeRouteUpdate (to, from, next) :

    在当前路由改变,并且该组件被复用时调用,可以通过this访问实例, next需要被主动调用,不能传回调

    • 对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,组件实例会被复用,该守卫会被调用
    • 当前路由query变更时,该守卫会被调用

    beforeRouteLeave (to, from, next) :

    航离开该组件的对应路由时调用,可以访问组件实例 thisnext需要被主动调用,不能传回调 用途:清除当前组件中的定时器,避免占用内存;当页面中有未关闭的窗口, 或未保存的内容时, 阻止页面跳转;保存相关内容到Vuex中或Session中 

    3、路由内的导航钩子

    主要用于写某个指定路由跳转时需要执行的逻辑

    beforeEnter

    有三个参数:to/from/next

    {
        path:'/',
        name:'Login',
        component:Login,
        beforeEnter:(to,from,next)=>{
            consloe.log("即将进入Login");
            next();
        }
    }