前端面试题(二)

高频面试题

vue-router原理

大型单页应用最显著特点之一就是采用前端路由系统,通过改变URL,在不重新请求页面的情况下,更新页面视图。

页面都是由组件组成的,只需要把组件和路径相对应起来,就能把组件渲染出来。

当用户点击router-link标签时,会去寻找它对应的to属性,它的to属性和js中配置的路径{path:'/home',component:Home}中的path一一对应,从而找到了匹配的组件,最后把组件渲染到router-view标签所在的地方。

前端路由时通过改变url,在不重新请求页面的情况下,更新页面视图。

目前在浏览器环境中实现这一功能主要有两种:

  • 利用url中的hash:

    在改变url的情况下,保证页面的不刷新。在2014年之前,大家是通过hash来实现路由,url hash就类似于:
    https://www.xxx.com/#/login
    这种# 后面hash值的变化,并不会导致浏览器向服务器发出请求,浏览器不发出请求也就不会刷新页面。另外每次hash值变化都会触发hashchange这个事件,通过这个事件我们就可以知道hash值发生了哪些变化。然后我们就可以通过监听hashchange来实现更新页面部分内容的操作

    1
    2
    3
    4
    window.addEventListener('hashchange', matchAndUpdate)
    function matchAndUpdate () {
    // todo 匹配 hash 做 dom 更新操作
    }
  • 利用H5中history:

    14年后,因为HTML5标准发布。多了两个Api,pushStatereplaceState,通过这两个api可以改变url地址且不会发送请求。同时还有popstate事件,通过这些就能用另一种方式实现前端路由了,原理和hash实现相同的。用了 HTML5 的实现,单页路由的 url 就不会多出一个#,变得更加美观。但因为没有 # 号,所以当用户刷新页面之类的操作时,浏览器还是会给服务器发送请求。为了避免出现这种情况,所以这个实现需要服务器的支持,需要把所有路由都重定向到根页面。
    内部使⽤window.history.pushState来处理url的变化,切换对应的组件

如果对具体代码实现感兴趣可以点击这里查看,vue-router源码实现

组件间通信方式

点击这里查看详情

vue2和vue3区别

  1. 目录结构

    vue-cli2.0与3.0在目录结构方面,有明显的不同

    vue-cli3.0移除了配置文件目录,config和build文件夹

    同时移除了static静态文件夹,新增了public文件夹,打开层级目录还会发现,index.html移动到public

  2. 配置项

    3.0中config文件已经被移除,但是多了.env.production.env.development文件,除了文件位置,实际配置起来和2.0没什么不同

    没了config文件,跨域需要配置域名时,从config/index.js挪到了vue.config.js中,配置方法不变

  3. 渲染

    Vue2.x使用的Virtual Dom实现的渲染

    Vue3.0不论是原生的html标签还是vue组件,他们都会通过h函数来判断,如果是原生html标签,在运行时直接通过Virtual Dom来直接渲染,同样如果是组件会直接生成组件代码

  4. 数据监听

    Vue2.x大家都知道使用的是es5的object.definepropertiesgettersetter实现的,而vue3.0的版本,是基于Proxy进行监听的,其实基于proxy监听就是所谓的lazy by default,什么意思呢,就是只要你用到了才会监听,可以理解为‘按需监听’,官方给出的诠释是:速度加倍,同时内存占用还减半。

  5. 按需引入

    Vue2.x中new出的实例对象,所有的东西都在这个vue对象上,这样其实无论你用到还是没用到,都会跑一变。而vue3.0中可以用ES module imports按需引入,如:keep-alive内置组件、v-model指令,等等。

简述vue2双向绑定原理

  1. 由页面->数据的变化:通过给页面元素添加对应的事件监听来实现

    1
    2
    3
    4
    5
    <input v-model="value" oninput="()=>this.handleInput($event)">

    function handleInput(e){
    this.value = e.target.value
    }
  2. 由数据->页面的变化:通过数据劫持(Object.defineProperty)+发布订阅模式来实现的
    具体流程:

    • Compile解析器会将⻚⾯上的插值表达式/指定翻译成对应Watcher以添加到订阅器维护的列表中

    • 通过Object.defineProperty劫持数据的变化,⼀旦数据源发⽣变化会触发对应的set⽅法

    • 在set⽅法中,通知订阅器(Dep)对象中维护的所有订阅者(Watcher)列表更新

    • 每⼀个Watch会去更新对应的⻚⾯

  3. 关于发布订阅模式

    发布订阅模式又叫观察者模式,他定义了一种一对多的关系,让多个观察者对象同时监听某一个主体对象的变化,当这个主题对象的状态发生变化的时候就会通知所有的观察者对象,使得他们能够自动更新自己。

自定义指令

Vue里面有许多内置的指令,比如v-ifv-show,这些丰富的指令能满足我们的绝大部分业务需求,不过在需要一些特殊功能时,我们仍然希望对DOM进行底层的操作,这时就要用到自定义指令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Vue.directive('focus', {
bind: function (el) {
// 每当指令绑定到元素上的时候,会立即执行这个bind 函数,只执行一次
// 注意: 在每个函数中,第一个参数永远是el,表示被绑定了指令的那个元素,这个el参数,是一个原生的JS对象
// 在元素刚绑定了指令的时候还没有插入到DOM中去的时候调用focus方法没有作用。因为,一个元素只有插入DOM之后才能获取焦点
// el.focus()
},
inserted: function (el) {
// inserted 表示元素 插入到DOM中的时候,会执行 inserted 函数【触发1次】。和JS行为有关的操作,最好在 inserted 中去执行,放置 JS行为不生效
el.focus()
},
updated: function (el) {
// 当VNode更新的时候,会执行 updated, 可能会触发多次
}
})
//使用
<input v-focus>

ES6特性

ES6 主要是为了解决 ES5 的先天不足,比如 JavaScript 里并没有类的概念,但是目前浏览器的 JavaScript 是 ES5 版本,大多数高版本的浏览器也支持 ES6,不过只实现了 ES6 的部分特性和功能。

  1. 新增箭头函数

    • 简化了写法, 少打代码,结构清晰
    • 明确了this。传统JS的this是在运行的时候确定的,而不是在定义的时候确定的;而箭头函数的this是在定义时就确定的,不能被改变,也不能被call,apply,bind这些方法修改。
  2. 块级作用域

    • ES6中的let声明的变量有块级作用域
    • ES5中是没有块级作用域的,并且var有变量提升的概念
    • let声明的变量在同一个作用域内只有一个
    • 要声明常量使用const
  3. 解构赋值

    ES6中变量的解构赋值,比如:

    1
    2
    let [a,b,c] = [0,1,2];
    let {foo:abc,bar} = {foo:'hello',bar:'hi'};
  4. Symbol

    1
    2
    3
    4
    5
    6
    7
    8
    9
    //ES6新增了一种Symbol数据类型,表示全局唯一的对象

    let a1 = Symbol();
    let a2 = Symbol();
    console.log(a1 === a2); //false a1和a2永远不相等

    let a3 = Symbol.for("a3");
    let a4 = Symbol.for("a3");
    console.log(a3 === a4); //true
  5. 模板字符串

    1
    2
    3
    4
    var name = "张三";
    var age = 12;
    var gender="男";
    let str = `姓名${name},年龄${age},性别${gender}`;
  6. 展开运算符

    1
    2
    3
    let arr1 = [1,2,3];
    let arr2 = [4,5,6];
    let arr3 = [...arr1,...arr2];
  7. ES6引入Class这个概念,让JS拥有其他面向对象语言的语法糖。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 继承传递参数
    class Parent{
    constructor(name='mukewang'){
    this.name=name;
    }
    }

    class Child extends Parent{
    constructor(name='child'){
    //先初始化父亲的信息,在初始化自己的信息
    super(name);
    this.type='child';
    }
    }
    console.log('继承传递参数',new Child('hello'));
  8. Promise

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    //ES6的Promise主要用于解决JS回调地狱问题

    var fs = require('fs')

    function pReadFile(filePath) {
    return new Promise(function (resolve, reject) {
    fs.readFile(filePath, 'utf8', function (err, data) {
    if (err) {
    reject(err)
    } else {
    resolve(data)
    }
    })
    })
    }

    pReadFile('./data/a.txt')
    .then(function (data) {
    console.log(data)
    return pReadFile('./data/b.txt')
    })
    .then(function (data) {
    console.log(data)
    return pReadFile('./data/c.txt')
    })
    .then(function (data) {
    console.log(data)
    })
  9. ES6的迭代器

    1
    2
    3
    4
    5
    6
    //for ... of 是ES6中新增加的语法,主要用来循环实现了Iterator接口类型的对象
    //for ... of 可以遍历Array、Set、Map不能遍历Object
    let arr = ['China', 'America', 'Korea']
    for (let o of arr) {
    console.log(o) //China, America, Korea
    }
  10. ES6模块化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //1.导出
    //导出单个成员
    export let name = 'leo';
    export let age= 30;
    let name= 'leo';
    let age= 30;
    let kk = "abc"
    //导出多个成员
    export {name, age};
    //导出默认成员
    export default kk;

    //2.引入
    import kk,{name,age} from 'a.js'

跨域

跨域问题产生的原因 :浏览器的同源策略导致了跨域。当我们在前端开发中使用ajax/fetch这些技术发送网络请求的时候,当协议、主机、端口有任何一个不一致的时候,则构成跨域。

跨域的作用 :用于隔离潜在恶意文件的重要安全机制

跨域问题的解决:

  1. jsonp,允许script加载第三方资源

  2. 在服务器使用cors实现跨域资源共享
    res.writeHead(200, {

    "Content-Type": "text/html; charset=UTF-8",
    "Access-Control-Allow-Origin":'http://localhost',
    'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
    'Access-Control-Allow-Headers': 'X-Requested-With, Content-Type'

    });

  3. 在前端的工程化项目(webpack)中,我们可以通过配置devserver的proxy来解决跨域访问的问题。他的原理是在本地开启一个服务器向数据服务器发送请求,因为服务器和服务器之间是没有跨域

  4. 但是因为webpack的devserver只在开发环境下有效,当项目发布上线之后仍然会有跨域问题,为了解决项目上线的跨域问题,我们配置服务器的反向代理(Apache/ngix)来实现跨域请求

  5. 除此之外,我还知道当项目打包成apk之后就不存在跨域问题了,所以如果项目要打包成apk,我们需要在项目中的所有请求中写全路径(此时我们可以配置axios.default.baseURL来解决)

  6. iframe 嵌套通讯,postmessage

继承

  1. 借助构造函数实现继承

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //  定义父类
    function Parent1 () {
    this.name = 'xxx',
    this.age = 18
    }
    // 定义子类
    function Child1 () {
    //通过call()方法改变Child1的this指向使子类的函数体内执行父级的构造函数从而实现继承效果
    Parent1.call(this)
    this.address = 'yyy'
    }
    // 构建子类的实例s1
    var s1 = new Child1()
    console.log(s1.name) //xxx

    缺点:该方法的实力(s1)无法使用父类(Parent1)的原型(prototype)中的属性和方法

  2. 借助原型链实现继承

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    function Parent2 () {
    this.name = 'xx',
    this.age = 19,
    this.play = [1,2,3]
    }
    // 一样在父类添加say方法
    Parent2.prototype = {
    say () {
    console.log('say')
    }
    }
    function Child2 (address) {
    this.address = 'yyy'
    }
    // 让子类的原型直接指向父类实例
    Child2.prototype = new Parent2()
    // 生成两个子类的实例s2、s3
    var s2 = new Child2()
    var s3 = new Child2()
    // s2实例继承了父类中的name属性
    console.log(s2.name) //xx
    // s2实例也同样继承了父类原型上的say方法
    console.log(s2.say()) //say

    缺点:在子类调用构造函数创建对象的时候,无法入参所有的属性值

  3. 组合继承

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function Parent5 () {
    this.name = 'xx',
    this.age = 20,
    this.play = [4,5,6]
    }
    function Child5 (name,age,address) {
    Parent5.call(this,name,age)
    this.address = address
    }
    // 比较关键的一步
    Child5.prototype = new Parent5()
    var c = new Child5("zhangsan",19,"无锡")
  4. 实例继承(为父类实例添加新特性,作为子类实例返回)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    function Animal (name) {
    // 属性
    this.name = name || 'Animal';
    // 实例方法
    this.sleep = function(){
    console.log(this.name + '正在睡觉!');
    }
    }
    // 原型方法
    Animal.prototype.eat = function(food) {
    console.log(this.name + '正在吃:' + food);
    };
    function Cat(){
    var instance = new Animal();
    instance.name = name || 'Tom';
    return instance;
    }
    // Test Code
    var cat = new Cat();
    console.log(cat.name);
    console.log(cat.sleep());
    console.log(cat instanceof Animal); // true
    console.log(cat instanceof Cat); // false
  5. 拷贝继承

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    // 定义一个动物类
    function Animal (name) {
    // 属性
    this.name = name || 'Animal';
    // 实例方法
    this.sleep = function(){
    console.log(this.name + '正在睡觉!');
    }
    }
    // 原型方法
    Animal.prototype.eat = function(food) {
    console.log(this.name + '正在吃:' + food);
    };
    function Cat(name){
    var animal = new Animal();
    // 遍历拷贝属性
    for(var p in animal){
    Cat.prototype[p] = animal[p];
    }
    Cat.prototype.name = name || 'Tom';
    }

    // Test Code
    var cat = new Cat();
    console.log(cat.name);
    console.log(cat.sleep());
    console.log(cat instanceof Animal); // false
    console.log(cat instanceof Cat); // true

宏微任务

事件循环的任务队列有宏任务队列和微任务队列,每次一个宏任务执行完毕的时候,都会把微任务队列中的微任务执行完毕之后才会再次执行下一个宏任务。

  • 宏队列:script(全局任务), setTimeout, setInterval, setImmediate, I/O, UI rendering.

  • 微队列:process.nextTick, Promise.then, Object.observer, MutationObserver.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    console.log("datagrand1");
    setTimeOut(()=>{
    console.log("datagrand2")
    });
    const p1 = new Promise(resolve=>{
    console.log("datagrand3");
    resolve();
    });
    p1.then(()=>{
    console.log("datagrand4");
    });
    console.log("datagrand5");
    const p2 = new Promise(resolve=>{
    console.log("datagrand6");
    resolve();
    });
    p2.then(()=>{
    console.log("datagrand7");
    });

打印顺序是:1,3,5,6,4,7,2

说几个常用的meta标签

详细看这篇

文章作者: Joker
文章链接: https://qytayh.github.io/2020/07/%E5%89%8D%E7%AB%AF%E9%9D%A2%E8%AF%95%E9%A2%98(%E4%BA%8C)/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Joker's Blog