常见前端面试指南

134 阅读35分钟

1.介绍js的基本数据类型

js一共有六种基本数据类型,分别是undefined、null、boolean、number、string、symbol、bigint,symbol表示独一无二且不可变的数据类型,它的出现我认为主要是为了解决可能出现的全局变量冲突问题。bigint是一种数字类型的数据,表示任意精度格式的整数,可以安全的操作大整数,

2.js有几种类型的值

  • 基本数据类型:undefined、null、boolean、number、string、symbol
  • 引用数据类型:array,object,Date等
  • 主要区别是存储位置不同,基本数据类型直接保存在栈中,而引用数据类型数据保存在堆中,在栈中会存放对应的指针指向堆中的值

3.null和undefined的区别

  • undefined表示未定义,null表示空对象,一般变量声明了但是还没有定义的时候返回undefined,null只要用于赋值给一些可能回返回对象的变量,作为初始化
  • 当使用typeof来判断类型时, null类型返回object,这是一个历史遗留问题,使用==判断时返回true,使用===判断时返回false

4.javascript原型,原型链

  • 在js中我们是使用构造函数来创建一个对象的,每一个构造函数的内部都有一个prototype属性值,这个属性值是一个对象,这个对象包括了由该构造函数的所有实例共享的属性和方法。当我们使用构造函数新建一个对象后,这个对象内部将包含一个指针,这个指针指向构造函数的prototype属性对应的值,这个指针被称为对象的原型
  • 可以使用Object.getPrototypeOf来获取对象的原型
  • 当我们访问一个对象的属性时,如果这个对象不存在这个属性,那么就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,这就是原型链的概念。原型链的尽头一般来说都是Object.prototype

5.js获取原型的方法

  • p.proto
  • p.constructor.prototype
  • object.getPrototypeOf

6.js中整数的安全范围是多少

  • 安全整数指的是,在这个范围内的整数转化为二进制存储的时候不会出现精度丢失,
  • Number.MAX_SAFE_INTEGER 最大安全整数2^53-1
  • Number.MIN_SAFE_INTEGER 最小安全整数-(2^53-1)

7.typeof NaN结果

NaN表示不是一个数字,typeof NaN 返回number NaN 是i 个特殊值,它和自身不相等,NaN != NaN 返回true

8.Array构造函数只有一个参数值时的表现

Array构造函数只带一个数字参数的时候,该参数会被当作为数组预设的长度,而非充当数组的第一个元素,创建出来的是一个空数组,只不过它的length属性被设置成为了指定的值

9.使用toString方法其他值到字符串的转换规则

  • null和undefined类型,null转换为'null', undefined转换为'undefined'
  • Boolean类型, true转化为'true', false转化为'false'
  • Number类型,直接转换

10.其它值到数字值的转换规则

  • Undefined 类型的值转换为NaN
  • Null类型的值转换为0
  • Boolean 类型的值,true转换为1,false转换为0
  • 对象类型会首先被转换为相应的基本类型值,如果返回的是非数字的基本类型值,则再遵循以上规则将其强制转换。
    • 为了将值转换为相应的基本数据类型,会首先检查该值内部是否有valueof()方法,如果有并且返回基本数据类型值,就使用该值进行强制类型转换。
    • 如果没有就使用toString()的返回值来进行强制类型转换
    • 如果valueof()和toString()都不返回基本类型值,则会产生typeError错误

11.解析字符串中的数字和将字符串强制转换为数字返回的结果都是数字,它们的区别是什么

  • 解析允许字符串(如parseInt)中包含非数字字符,解析按照从左到右,如果遇到非数字字符则停止
  • 强制转换(如Number)不允许出现非数字字符,否则会失败并返回NaN

12.js生成随机数的方法

  • 随机浮点数的生成

    • 生成[0,1)范围内的随机数
        Math.random()
    
    • 生成[n,m)范围的随机数
        Math.random()*(m-n) + n
    
  • 随机整数生成

    • Math.round(num) 将num四舍五入取整
    • Math.floor(num) 向下取整
    • 随机生成0、1两个整数
        Math.round(Math.random())
    
    
    • 随机生成[0, n) 范围内的随机数
        Math.floor(Math.random()*n)
    
    
    • 随机生成[1, n] 范围内的随机整数
        Math.floor(Math.random()*n) + 1
    
    
    • 随机生成[min, max] 范围内的随机整数
        Math.floor(Math.random() * (max-min+1))+min
    
    

13.实现数组的随机排序

// 1. 随机从数组中取出一个元素放入新数组中
function randomSort(arr) {
    var result = [];
    while(arr.length > 0) {
        var randomIndex = Math.floor(Math.random() * arr.length);
        result.push(arr[randomIndex])
        arr.splice(randomIndex, 1)
    }
    return result
}

// 2. 随机交换数组内的元素
function randomSort(arr) {
    var index, randomIndex, temp, len = arr.len;
    for (index = 0; index < len; index++) {
        randomIndex = Math.floor(Math.random() * (len-index)) + index;
        temp = arr[index]
        arr[index] = arr[randomIndex];
        arr[randomIndex] = temp
    }
    return arr
}

14.js创建对象的几种方式

  • 工厂模式,
    • 用函数来封装创建对象的细节,从而通过调用函数来达到复用的目的
    • 缺点:创建出来的对象无法和某个类型联系起来,只是简单的封装了复用代码,没有建立起对象和类型之间的关系
  • 构造函数模式
    • js中每一个函数都可以作为构造函数,只要一个函数是通过new调用的,那么我们就可以把它称为构造函数。执行构造函数首先会创建一个对象,然后将对象的原型指向构造函数的prototype属性,然后将指向上下文中的this指向这个对象,最后指向整个函数,如果返回值不是对象,则返回新建的对象
  • 原型模式
    • 每一个函数都有一个prototype属性,这个属性是一个对象,它包含了通过构造函数创建的所有实例都能共享的属性和方法,因此可以使用原型对象来添加公用属性和方法,从而实现代码复用

    • 缺点:没有办法通过传入参数来初始化;另一个是如果存在一个引用类型如Array这样的值,所以实例将共享一个对象,一个实例对引用类型值的改变会影响所有的实例

15.js继承的几种方式

  • 原型链继承

    • 使用原型链的方式实现继承,
    • 包含有引用类型的数据时,会被所有实例对象共享,容易造成修改的混乱
    • 创建子类型时无法传递参数
        function Parent() {
            this.name = '父类';
            this.introduce = function () {
                console.log('my name is' + this.name)
            }
        }
        
         // 子类
        function Child() {
            this.childname = '子类'
        }
        // 核心代码
        Child.prototype = new Parent()
    
    
  • 构造函数继承

    • 在子类的函数中调用父类的构造函数来实现
    • 无法实现函数方法的复用;
    • 父类原型定义的方法,子类也没有办法访问到
       function Parent() {
           this.name = '父类';
           this.introduce = function () {
               console.log('my name is' + this.name)
           }
       }
       Parent.prototype.say = function () {
           console.log('父类原型上的方法')
       }
       
       // 子类
       function Child() {
           this.childname = '子类'
           Parent.call(this)
       }
    
    
  • 组合继承

    • 将原型链和构造函数组合起来使用的一种方式
    • 会调用两次父类的构造函数
    function Parent() {
           this.name = '父类';
           this.introduce = function () {
               console.log('my name is' + this.name)
           }
       }
       Parent.prototype.say = function () {
           console.log('父类原型上的方法')
       }
       
        // 子类
       function Child() {
           this.childname = '子类'
           Parent.call(this) // 第二次调用parent
       }
       // 核心代码
       Child.prototype = new Parent() // 第一次调用parent
    
    
    
  • 寄生组合式继承

    • 使用父类的原型的副本来作为子类的原型,避免了创建不必要的属性
        function Person(name){
            this.name = name
        }
        Person.prototype.sayName = function () {
            console.log('my name is' + this.name)
        }
        function Student(name) {
            Person.call(this, name);
        }
        Student.prototype = Object.create(Person.prototype);
        Student.prototype.constructor = Student
    
    

16. js的作用域链

  • 作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,我们可以访问到外层环境的变量和函数
  • 作用域链的本质是一个指向变量对象的指针列表,变量对象是一个包含了执行环境中所有变量和函数的对象,作用域链的前端始终都是当前执行上下文的变量对象,全局执行上下文的变量对象始终是作用域链的最后一个对象
  • 当我们查找一个变量时,如果当前执行环境中没有找到,我们可以沿着作用域链向后查找

17. 对this的理解

  • this是执行上下文中的一个属性,它指向最后一次调用这个方法的对象
  • 有四种调用模式
    • 函数调用模式,当函数不是一个对象的属性,直接作为函数调用时,this指向全局对象
    • 方法调用模式,当一个函数作为一个对象的方法来调用时,this指向这个对象
    • 构造函数调用,如果一个函数用new调用时,函数执行前会新创建一个对象,this指向这个新创建的对象
    • apply、call、bind调用: 显示的指定调用函数的this指向

18. 什么是dom和bom

  • dom指的是文档对象模型,这个对象主要定义了处理网页内容的方法和接口
  • bom指的是浏览器对象模型,这个对象主要定义了与浏览器进行交互的方法和接口

19. 事件监听器函数

const EventUtils = {
  // 视能力分别使用dom0||dom2||IE方式 来绑定事件
  // 添加事件
  addEvent: function(element, type, handler) {
    if (element.addEventListener) {
      element.addEventListener(type, handler, false);
    } else if (element.attachEvent) {
      element.attachEvent("on" + type, handler);
    } else {
      element["on" + type] = handler;
    }
  },

  // 移除事件
  removeEvent: function(element, type, handler) {
    if (element.removeEventListener) {
      element.removeEventListener(type, handler, false);
    } else if (element.detachEvent) {
      element.detachEvent("on" + type, handler);
    } else {
      element["on" + type] = null;
    }
  },

  // 获取事件目标
  getTarget: function(event) {
    return event.target || event.srcElement;
  },

  // 获取 event 对象的引用,取到事件的所有信息,确保随时能使用 event
  getEvent: function(event) {
    return event || window.event;
  },

  // 阻止事件(主要是事件冒泡,因为 IE 不支持事件捕获)
  stopPropagation: function(event) {
    if (event.stopPropagation) {
      event.stopPropagation();
    } else {
      event.cancelBubble = true;
    }
  },

  // 取消事件的默认行为
  preventDefault: function(event) {
    if (event.preventDefault) {
      event.preventDefault();
    } else {
      event.returnValue = false;
    }
  }
};

20. 事件是什么?ie和火狐事件机制又啥区别?如何阻止冒泡

  • 事件是用户操作网页时发生的操作,比如点击事件等等,事件被封装成一个event对象,包含了该事件发生时的所有信息以及可以对事件进行的操作
  • ie支持事件冒泡,火狐支持事件冒泡和事件捕获
  • event.stopPropagation()或者ie的event.canceBubble = true;

21. 三种事件模型

  • 原始事件模型
    • 这种模型不会传播,可以在网页中直接定义监听函数,也可以通过js属性来指定监听函数
  • IE事件模型
    • 在该事件模型中,一次事件有两个过程,事件处理阶段和事件冒泡阶段。
      • 事件处理阶段会首先执行目标元素绑定的监听事件
      • 事件冒泡指的是事件从目标元素冒泡到document,依次检查经过的节点是否绑定了事件监听函数,有则执行
      • 这种模型通过attachEvent来添加监听函数,
  • DOM2级事件模型
    • 该事件模型中,一次事件有三个过程,事件捕获、事件处理、事件冒泡
      • 事件捕获是指从document一直向下传播到目标元素,依次检查经过的节点是否绑定了事件监听函数,有则执行
      • 事件处理阶段会首先执行目标元素绑定的监听事件
      • 事件冒泡指的是事件从目标元素冒泡到document,依次检查经过的节点是否绑定了事件监听函数,有则执行
      • 这种模型通过addEventListener来添加监听函数,传递三个参数,其中第三个参数可以指定事件是否在捕获阶段执行,默认false,表示在冒泡阶段执行。

22.事件委托是什么

  • 事件委托的本质是利用了浏览器事件冒泡的机制,因为事件在冒泡过程中会上传到父节点,并且父节点可以通过事件对象获取到目标节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件
  • 使用事件代理我们可以不必要为每一个子元素都添加点击事件,这样减少了内存消耗,并且使用事件代理我们还可以实现事件的动态绑定,比如新增一个子节点,我们不需要单独地为它添加一个监听事件,它所发生的事件会交给父元素中的监听函数来处理。

23.如何判断一个对象是否属于某个类

  • 使用indexof运算符判断构造函数的prototype是否出现在原型链中的任何位置
  • 如果需要判断的是某个内置的引用类型的话,可以使用Object.prototype.toString.call()方法打印对象的[[class]]属性

24.instanceof的作用

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

function myInstanceOf(left, right) {
    let proto = Object.getPrototypeOf(left) // 对象原型
    let prototype = right.prototype // 构造函数的prototype对象
    while(true) {
        if (!proto) return false
        if (proto === prototype) return true
        proto = Object.getPrototypeOf(proto)
    }
}

25.new操作符具体干了什么?如何实现?

  • 首先创建一个空对象
  • 设置原型,将对象的原型设置为函数的prototype对象
  • 让函数的this指向这个对象,执行构造函数代码
  • 判断函数返回值类型,如果是值类型,返回创建的对象,如果是引用类型,返回这个引用类型对象
function objectFactory() {
    let newObject = null,
        constructor = Array.prototype.shift.call(arguments),
        result = null
    if (typeof constructor !== 'function') {
        return
    }
    newObject = Object.create(constructor.prototype)
    result = constructor.apply(newObject, arguments)
    let flag = result && (typeof result === 'object' || typeof result === 'function')
    return flag ? result : newObject
}


26. 延迟加载的方式有哪些

js延迟加载,也就是等页面加载完成之后再加载js文件,有助于提高页面加载速度

  • 将js脚本放在文档的底部,来使js脚本尽可能地在最后加载执行
  • 给js脚本添加defer属性,让脚本的加载与文档的解析同步解析,然后在文档解析完成后再执行这个脚本文件,多个设置defer属性的脚本按规范来说是顺序执行的
  • 给js脚本添加async属性,让脚本异步加载,不会阻塞页面的解析过程,脚本加载完成之后会立即执行js脚本,如果文档解析没有完成的话同样会阻塞,多个async属性的脚本的执行顺序是不可预测的

27.谈谈浏览器的缓存机制

浏览器缓存机制指的是通过在一段时间内保留浏览器接收到的资源副本,如果在资源有效时间内,发起了对这个资源的再一次请求,那么浏览器就会直接使用缓存的副本,而不是向服务器重新发起请求,使用web缓存可以有效的提高页面的打开速度,减少不必要的网络带宽的消耗 web资源的缓存分为两种,分别是强缓存和协商缓存

  • 强缓存
    • 使用强缓存时,如果缓存资源有效,则直接使用缓存资源,不必要再向服务器发起请求
    • 在http1.0中,服务器通过在响应头中添加expires属性,来指定资源过期时间,在过期时间内,该资源可以被缓存使用,这个时间是一个绝对时间,它是服务器时间,如果客户端的本地时间和服务器时间存在差异,就可能会影响缓存命中的结果
    • http1.1中,使用cache-control来对资源缓存做更精确的控制
      • max-age 指定资源能被缓存的时间大小,它是一个相对时间
      • private 规定资源只能被客户端缓存,不能被代理服务器缓存
      • no-store 规定资源不能被缓存
      • no-cache 表示资源可以被缓存,但是每次都需要向服务器进行确认
  • 协商缓存
    • 使用协商缓存时,会先向服务器发送一个请求,如果资源没有发生修改,返回304状态,让浏览器使用本地缓存的资源,如果资源发生了修改,则返回修改的资源,协商缓存可以通过两种方式来设置,请求头的Etag和last-modified两个属性
    • 服务器通过在响应头中添加last-modified属性来表示资源最后一次修改时间,当浏览器再次发起请求时,请求头会带上if-modified-since属性,属性值为上一次资源返回的last-modified值,服务器会通过这个属性和资源最后一次修改时间进行比较,判断资源是否有修改
      • 缺点:Last-Modified标注的最后修改时间只能精确到秒级,如果文件在1s内修改多次,文件已改变,但是last-modified没有改变
    • Etag属性,服务器在返回资源的时候,在头信息添加了Etag属性,当资源发生改变时,这个值也会改变,在下一次请求资源时,浏览器会在请求头上添加一个if-none-match属性,这个属性的值是上次资源返回的Etag,服务器回根据这个值来和资源的Etag进行比较,以此判断资源是否发生改变

28.Ajax解决浏览器缓存问题

  • 在ajax发送请求前加上 setRequestHeader('if-Modified-Since', '0')
  • 在ajax发送请求前加上 setRequestHeader('Cache-Control', 'no-cache')
  • 在请求url后加上时间戳

29.同步和异步区别

  • 同步指的是当一个进程在执行某个请求时,如果这个请求需要一段时间才能返回,那么这个进程就会一直等待下去,知道消息返回再继续向下执行
  • 异步指的是当一个进程在执行某个请求时,如果这个请求需要一段时间才能返回,这个时候进程会继续向下执行,不会阻塞等待消息返回,当消息返回系统时再通知进程进行处理

30.简单谈一下cookie

  • cookie是服务端提供的一种用于维护会话状态信息的数据,通过服务器发送到浏览器,浏览器保存到本地,当下一次有同源请求时,会自动添加到请求头部,发送给服务端,可以用来实现记录用户登录状态等功能。cookie一般可以存储4k大小的数据
  • 服务端可以使用set-cookie的响应头部来配置cookie信息,一条cookie包括了9个属性值
    • name和value分别是cookie的名字和值
    • expires cookie过期时间
    • domain 域名
    • path 路径
    • secure 规定cookie只能在确保安全的情况下传输
    • httponly 规定cookie只能被服务器访问,不能被js访问
    • samesite 限制第三方cookie,防止csrf攻击,从而减少安全风险

31.模块化开发如何做

  • 我对模块化的理解是,一个模块是实现一个特定功能的一组方法,在最开始的时候,js只实现一些简单的功能,所以并没有模块化的概念,随着程序越来越复杂,代码的模块化开发变得越来越重要
  • 由于函数具有独立的作用域的特点,最原始的写法是使用函数作为模块,几个函数作为一个模块,这种方式容易造成全局作用域污染
  • 后面提出使用对象写法,通过将函数作为对象的一个方法来实现,这种方式会暴露所有的模块成员,外部代码可以修改内部属性的值
  • 现在最常用的是使用立即执行函数的写法,通过利用闭包来实现模块私有作用域的建立,同时不会对全局作用域造成污染

32.js的几种模块化方案

  • commonjs方案,它通过require来引入模块,通过module.exports定义模块输出,这种模块加载方案是服务端的解决方案,它是以同步的方式来引入模块,因为在服务端文件都存储在本地磁盘,所以读取非常快,所以以同步的方式加载没有问题,但是如果在浏览器端,由于模块的加载使用网络请求,因此使用异步加载方式更加合适
  • AMD方案,采用异步加载的方式来加载模块,模块的加载不影响后面语句的执行,所有依赖这个模块的语句都定义在一个回调函数里,等到加载完成后再执行回调函数,require.js实现了AMD规范
  • CMD方案,异步加载模块,sea.js实现了CMD规范
  • es6方案,使用import和export的形式来导入导出模块

33.AMD和CMD的区别

  • 第一个方面是模块定义时对依赖的处理不同,AMD推崇依赖前置,在定义模块的时候就要声明其依赖的模块。而CMD推崇就近依赖,只有在用到某个模块时再去require
  • 第二个方面是对依赖模块的执行时机处理不同,首先AMD和CMD对于模块的加载方式都是异步加载,不过他们的区别在于模块的执行时机,
    • AMD在依赖模块加载完成后就直接执行依赖模块,依赖模块的执行顺序和我们书写的顺序不一定一致。
    • CMD在依赖模块加载完成后并不执行,只是下载而已,等到所有依赖模块都加载好后,进入回调函数逻辑,遇到require语句的时候再执行对应的模块,这样模块的执行顺序就喝我们书写的顺序保持一致了。
// CMD
define(function (require, exports, module) {
    var a = require('./a');
    a.dosomething()
    var b = require('./b');
    b.dodomething()
})

// AMD
define(['./a', './b'], function (a, b) {
    // 依赖必须一开始就写好
    a.dosomething()
    b.dosomething()
})


34. ES6 模块化与 CommonJS 模块化区别

35. document.write和innerHTML的区别

  • document.write的内容会代替整个文档内容,会重写整个页面
  • innerHTML 的内容只是替代指定元素的内容,只会充重写页面中的部分内容

36. es6 class的意义

es6新增的class只是为了补充js种缺少的一些面向对象语言的特性,本质来说它只是一种语法糖,不是一个新的东西,背后还是原型继承的思想。通过加入class可以有利于我们更好的组织代码

37. js对类数组的定义

  • 一个拥有length属性和若干索引属性的对象就可以被称为类数组对象,类数组对象和数组类似,但是不能调用数组的方法
  • 常见的数组对象有arguments和dom方法的返回结果
  • 常见的类数组转数组方法
    • 通过call调用数组的slice方法来实现转换
        Array.prototype.slice.call(arrayLike)
    
    • 通过call调用数组的splice方法来实现转换
        Array.prototype.splice.call(arrayLike, 0)
    
    • 通过apply调用数组的concat方法来实现转换
        Array.prototype.concat.apply([], arrayLike);
    
    
    • 通过Array.from
        Array.from(arrayLike)
    
    

38. 简单介绍下v8引擎的垃圾回收机制

  • v8的垃圾回收机制基于分代回收机制,这个机制又基于世代假说,这个假说有两个特点,基于这两个特点,v8引擎将内存分为了新生代和老生代

    • 一是新生的对象容易早死
    • 另一个是不死的对象会活的更久
  • 新创建的对象或者只经历过一次的垃圾回收的对象被称为新生代,经历过多次垃圾回收的对象被称为老生代

  • 新生代被分为from和to两个空间,to一般是闲置的,当from空间满了的时候会执行scavenge算法进行垃圾回收。当我们执行垃圾回收算法的时候应用逻辑将会停止,等垃圾回收结束后再继续执行。这个算法分为三步

    • 先检查from空间的存活对象,如果对象存活则判断对象是否满足晋升到老生代的条件,如果满足条件则晋升到老生代,如果不满足则移动到to空间
    • 如果对象不存活,则释放对象的空间
    • 最好将form和to空间角色进行交换
  • 新生代晋升到老生代的条件

    • 第一个是判断对象是否已经经历过一次scavange回收,若经历过,则将对象从from空间复制到老生代中,若没有,则复制到to空间
    • 第二个是to空间的内存使用占比是否超过限制,当对象从from空间复制到to空间时,如果to空间使用超过25%,则对象直接晋升到老生代中,设置这个限制主要是因为算法结束后,两个空间会交换位置,如果to空间过小,会影响后续的内存分配
  • 老生地啊采用了标记清除法和标记压缩法

    • 标记清除法首先会对内存中存活的对象进行标记,标记结束后清除那些没有标记的对象
    • 由于标记结束后会造成很多的内存碎片,不便于后面的内存分配,所以为了解决内存碎片问题引入了标记压缩法

39. 哪些操作会造成内存泄漏

  • 意外的全局变量
    • 由于使用未声明的变量,而意外创建了一个全局变量,而使这个变量一直保留在内存中无法释放
  • 计时器
    • 设置了setInterval定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直保存在内存中,而无法被回收
  • dom引用
    • 获取一个dom元素的引用,然后把这个元素删除,由于我们一直保留了对这个元素的引用,所以它也无法被回收
  • 闭包
    • 不合理使用闭包,导致某些变量一直被留在内存当中。

40. 移动端的点击事件有延迟,时间是多少,如何解决

移动端有300ms的延迟是因为移动端会有双击缩放操作,因此浏览器在click之后要等待300ms,看用户有没有下一次点击,来判断这次点击是否是双击

  • 解决方案
    • 通过meta标签禁用网页缩放
    • 通过meta标签将网页的viewport设置为ideal viewport

41.什么是前端路由

  • 前端路由就说把不同的路由对应不同的内容或页面的任务交给前端,之前是通过服务端根据url的不同返回不同的页面实现的
  • 在单页面应用中,大部分页面结构不变,只改变部分内容时使用前端路由
  • 优点
    • 用户体验好,不需要每次都从服务器全部获取,可以快速将页面展示给用户
  • 缺点
    • 单页面无法记住之前滚动的位置,无法在前进后退时记住
  • 前端路由一共有两种方式,一种通过hash值的方式,一种是通过pushState方式

42.检测浏览器版本有哪些方式

  • 检测window.navigator.userAgent的值,但是这种方式不可靠,因为userAgent是可以被修改的
  • 功能检测,根据每个浏览器独有的特性来判断,如ie独有的ActiveXObject

43.介绍下js的节流和防抖

  • 防抖,在事件被触发n秒后再执行回调,如果在这n秒内事件又被触发,则重新计时
  • 节流,规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效
// 防抖
function debounce(fn, wait) {
    var timer = null
    return function() {
        var content = this,
        args = arguments
        if (timer) {
            clearTimeout(timer)
            timer = null
        }
        timer = setTimeout(() => {
            fn.apply(content, args)
        }, wait)
    }
}

// 节流
function throuttle(fn, wait) {
    var preTime = Date.now()
    return function () {
        var content = this,
        args = arguments,
        nowTime = Date.now()
        if (nowTime - preTime > wait) {
            preTime = Date.now()
            fn.apply(content, args)
        }
    }
}


44. Object.is() 与‘===’和‘==’的区别

  • 两等号判断,会在比较时进行类型转换然后再进行比较
  • 三等号判断,比较时不进行类型转换,类型不同返回false
  • Object.is()在三等判断的基础上,特殊处理了一些情况
    • 保证-0和+0不再相等
    • Object.is(NaN, NaN)会返回true

45. 事件循环是什么

  • 任务分为两种,一种同步任务,一种是异步任务,
  • 所有的同步任务都在主线程上执行,形成一个执行栈
  • 主线程之外,还存在一个任务队列,只要异步任务有了结果,就在任务队列中放置一个事件
  • 一旦执行栈中的所有同步任务执行完成,系统就会读取任务队列,将事件放入执行栈执行
  • 主线程开始执行一段代码,将代码放入执行栈中执行,同步代码优先执行,执行过程中,当遇到任务源时,判断是宏任务还是微任务
  • 如果是宏任务放入宏任务队列,如果是微任务,放入微任务队列
  • 同步代码执行完成,执行栈空闲,检查微任务队列是否有可执行任务,如果有,依次执行所有微任务队列中的任务
  • 如果没有,则检查宏任务队列中释放有可执行的宏任务,如果有,取出队列最前面的宏任务,放入执行栈中开始执行,然后重复以上步骤,直到宏任务队列中所有任务执行结束
  • 宏任务
    • script脚本
    • setTimeout
    • setInterval
    • I/O操作
    • UI渲染
  • 微任务
    • promise回调
    • node中的process.nextTick

46. js中的深浅拷贝

function shallowCopy (object) {
    if (!object || typeof object !== 'object') return
    let newObject = Array.isArray(object) ? [] : {}

    for (let key in object) {
        if (object.hasOwnProperty(key)) {
            newObject[key] = object[key]
        }
    }
    return newObject
}

function deepCopy (object) {
    if (!object || typeof object !== 'object') return
    let newObject = Array.isArray(object) ? [] : {}

    for (let key in object) {
        if (object.hasOwnProperty(key)) {
            newObject[key] = deepCopy(object[key])
        }
    }
    return newObject
}

  • 浅拷贝指的是将一个对象的属性复制到另一个对象,如果有的属性的值为引用类型,那么会将这个引用的地址复制给对象,因此两个对象会有同一个引用类型的引用
  • 深拷贝如果遇到属性值为引用类型时,会新建一个引用类型并将对应的值复制给它,可以使用JSON.parse(JSON.stringfy)来实现,但是由于json的对象格式比js的对象格式更加严格,所以如果属性值中出现函数或者symbol类型的值,会转换失败

47.手写call、apply、bind函数

Function.prototype.myCall = function (context) {
    // 获取参数
    let args = [...arguments].slice(1),
    retult = null;
    // 判断content是否传入,没有传入则设置为window
    context = context || window;
    // 将调用函数设置为对象的方法
    context.fn = this;
    // 调用函数
    result = context.fn(...args);
    delete context.fn
    return retult
}

Function.prototype.myApply = function (context) {
    let result = null;
    context = context || window;
    context.fn = this;
    if (arguments[1]) {
        result = context.fn(...arguments[1])
    } else {
        result = context.fn()
    }
    delete context.fn
    return result
}


Function.prototype.myBind = function (context) {
    var args = [...arguments].slice(1),
    fn = this;
    return function Fn() {
        return fn.apply(this instanceof Fn ? this : context, args.content(...arguments))
    }
}

48.函数柯里化

  • 函数柯里化是指一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术
function curry(fn, args) {
    let length = fn.length;
    args = args || [];
    return function () {
        let sunArgs = args.slice(0);
        sunArgs = sunArgs.concat(...arguments);
        if (sunArgs.length >= length) {
            return fn.apply(this, sunArgs)
        } else {
            return curry.call(this, fn, sunArgs)
        }
    }
}

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

  • 当计算机计算0.1+0.2的时候,实际上计算的是这两个数字在计算机里所存储的二进制,0.1和0.2在转换为二进制的时候会出现位数无限循环的情况,js中是以64位双精度格式来存储数字的,只有53位的有效数字,超过这个长度的位数会被截取掉,这样就造成了精度丢失的问题,这是第一个会造成精度丢失的地方。两个数字进行相加运算后,得到的结果可能会超过53位有效数字,因此超过的位数也会被截取掉,也会造成精度丢失
  • 将小数转换为整数后,再进行计算,计算完再转换为对应的小数

50. 什么是xss攻击,如何防范xss攻击

  • xss攻击指的是跨站脚本攻击,是一种代码注入式攻击,攻击者通过在网站注入恶意脚本,使之在用户的浏览器上运行,从而盗取用户的信息如cookie等
  • xss的本质是因为网站没有对恶意代码进行过滤,与正常的代码混合在一起了,浏览器没有办法分辨哪些脚本是可信的,从而导致了恶意代码的执行
  • xss一般分为存储型、反射型和dom型
    • 存储型指的是恶意代码提交到了网站的数据库中,当用户请求数据时,服务器将其拼接为html返回给了用户,从而导致恶意代码执行
    • 反射型指的是攻击者构建了特殊的url,当服务器收到请求后,从url中获取数据,拼接到html后返回,从而导致恶意代码执行
    • dom型指的是攻击者构建了特殊的url,用户打开网站后,js脚本从url中获取数据,从而导致恶意代码执行
  • xss攻击的预防可以从两个方面入手,一个是恶意代码提交的时候,一个是浏览器执行恶意代码的时候
    • 对存入数据库的数据都进行转义处理,但是一个数据可能在多个地方使用,有的地方不需要转移,由于我们没有办法判断数据最好的使用场景,所以直接在输入端进行恶意代码的处理,其实是不可靠的
    • 对需要插入到html中的代码最好充分的转义
    • 使用csp,csp的本质是建立一个白名单,告诉浏览器哪些外部资源可以加载和执行,从而预防恶意代码的注入攻击
    • 还可以对敏感信息进行保护,比如cookie使用http-only,使得脚本无法获取cookie信息

51. 什么是csp

  • csp指的是内容安全策略,它的本质是建立一个白名单,告诉浏览器哪些外部资源可以加载和执行。我们只需要配置规则,如何拦截由浏览器自己来实现
  • 开启csp有两种方式
    • 一种是设置http首部中的content-security-policy
    • 一种是设置meta标签的方式

52. 什么是csrf攻击,如何防范

  • csrf攻击指的是跨站请求伪造攻击,攻击者诱导用户进入一个第三方网站,然后该网站向被攻击网站发送跨站请求,如果用户在被攻击网站中保存了登陆信息,那么攻击者就可以利用这个登陆状态,绕过后台的用户验证,冒充用户向服务器执行一些操作
  • csrf攻击一般分为三种
    • 第一种是get类型的csrf攻击,比如在网站中的一个img标签中构建一个请求,当用户打开这个网站的时候就会自动发起提交
    • 第二种是post类型的csrf攻击,比如构建一个表单,然后隐藏它,在用户进入页面,自动提交表单
    • 第三种是链接型的csrf攻击,比如在a标签的href属性里构建一个请求,诱导用户点击
  • csrf防护
    • 第一种是同源检测,服务器根据http请求头中orgin或者referer信息来判断请求是否为允许访问的站点,当orgin或者referee信息都不存在时,直接阻止,
    • 第二种是使用token进行验证,服务器向用户返回一个随机数token,当网站再次发起请求时,在请求参数中加入服务端返回的token,然后服务器对这个token进行验证,
      • 需要给网站中的每个请求都加上token,比较繁琐
      • 如果我们的请求经过负载均衡转移到了其他服务器,但是这个服务器的session中没有保留这个token,就没法验证了
    • 第三种设置cookie属性的时候设置samesite,限制cookie不能被第三方使用

53. 什么是samesite cookie属性

  • samesite cookie表示同站cookie,避免cookie被第三方网站使用
  • 将samesite设置为strict,这种模式为严格模式,表示这个cookie任何请求下都不可能被第三方使用
  • 将samesite设置为lax,这种模式为宽松模式,如果这个请求是get请求,并且这个请求改变了当前页面或者打开了新页面,那么这个cookie可以作为第三方cookie。其他情况下都不能作为第三方cookie

54. 什么是点击劫持

  • 点击劫持是一种视觉欺骗的攻击手段,攻击者将需要攻击的网站通过iframe嵌套的方式嵌入自己的网页,并将iframe设置为透明,在页面中透出一个按钮诱导用户点击
  • 可以通过在http头中设置,x-frame-options来防御用iframe嵌套的点击劫持攻击

55. 什么是mvvm、mvc、mvp

  • mvc、mvp、mvvm是三种常见的软件架构设计模式,主要通过分离关注点的方式来组织代码结构,优化开发效率
  • mvc通过分离model、view和controller的方式来组织代码结构,其中view负责页面显示逻辑,model负责存储页面的业务数据,以及对应数据的操作。controller负责监听view的用户事件,得到数据后渲染view
    • 随着逻辑的复杂,这样处理比较难调试,由于view一定要允许在ui环境下,而且model或controller和view强耦合。没有办法单独验证逻辑的正确性,
  • mvp,其中view负责页面显示逻辑,model负责存储页面的业务数据。presenter会调用view层提供的接口去渲染model
  • mvvm, 比起mvp中view需要自己提供api,mvvm在viewmodel中构建一组状态数据,作为view状态的抽象。然后通过双向数据绑定使得vm中的状态数据与view中的显示状态保持一致。这样vm中的展示逻辑只需要修改对应的状态数据,就可以控制view的状态

56. vue双向数据绑定原理

  • vue通过双向数据绑定,来实现了view和model的同步更新,vue的双向绑定主要是通过使用数据劫持和发布订阅者模式来实现的
  • 首先通过object.defineProperty方法对model数据各个属性添加访问器属性,以此来实现数据劫持,因此当model中的数据发生变化的时候,我们可以通过配置的setter和getter方法来实现对view层数据更新的通知
  • 数据在html模版中一共有两种绑定情况,一种使用v-mode来对value进行绑定,一种是作为文本绑定,在对模版引擎进行解析的过程中
    • 如果遇到元素节点,并且包括v-model的话,就从model中去获取v-model所对应的属性的值,并赋值给元素的value值,然后给这个元素设置一个监听事件,当view中元素的数据发生变化的时候触发这个事件,通知model中对应的属性的值进行更新
    • 如果遇到了绑定的文本节点,我们使用model中对应的属性的值来替换这个文本,对于文本节点的更新,我们使用了发布订阅模式,属性作为一个主题,我们为这个节点设置一个订阅者对象,将这个订阅者对象加入这个属性主题的订阅者列表中,当model层数据发生改变的时候,model作为发布者向主题发出通知,主题收到通知再向它所有的订阅者推送,订阅者收到通知后更改自己的数据

57. 什么是虚拟dom,为什么虚拟dom比原生dom快

  • 首先对我们将要插入到文档中的dom树结构进行分析,使用js对象将其表示出来,比如一个元素对象,包含TagName、props和children这些属性,然后我们将这个js对象树保存起来,最好再将dom片段插入到文档中
  • 当页面的状态发生改变时,我们需要对页面的dom的结构进行调整的时候,我们首先根据变更的状态,重新构建起一颗对象树,然后将这颗心的对象树和旧的对象树进行比较,记录下两颗树的差异
  • 最好将记录的有差异的地方应用到真正的dom树中去,更新试图
  • 我认为虚拟dom这种方法对于我们需要有大量的dom操作的时候,能够很好的提高我们的操作效率,通过在操作前确定需要做的最小修改,尽可能的减少dom操作带来的重流和重绘的影响,其实虚拟dom并不一定比我们真实的操作dom要快,这种方法的目的是为了提高开发时的可维护性,在任意情况下,都能保证一个尽量小的性能消耗去进行操作

58. 什么是requestAnimationFrame

  • 随着前端的发展,css已经可以实现非常多的动画了,但是仍然存在一些无法实现的动画,比如页面滚动。通常解决方案都是使用setInterval设置定时器来实现动画特效,
  • 由于js是单线程,所以定时器实现实在当前任务队列完成后再执行定时器的回调的,假如当前队列任务执行时间大于定时器设置的延迟时间,那么定时器就不可靠了
  • 定时器实现的js动画,因为浏览器压根就无法办证每一帧渲染的时间间隔,一般情况下,每秒平均刷新次数能够达到60帧,就能够给人流畅的体验,即每过1000/60毫秒渲染新一帧即可,但是这一点定时器是无法保证的。
  • requestAnimationFrame的作用就是让浏览器流畅的执行动画效果,通过这个api,可以告诉浏览器某个js代码要执行动画,浏览器收到通知后,则会允许这些代码的时候进行优化,实现流畅的效果,开发者不需要关心刷新频率的问题
  • requestAnimationFrame接收一个动画执行函数作为参数,这个函数的作用是仅执行一帧动画的渲染,并根据条件判断是否结束,如果动画没有结束,则继续调用requestAnimationFrame并将自身作为参数传入
  • 在高版本浏览器中,开发人员不需要操心每一帧动画渲染的时间间隔问题,而针对低版本浏览器,则需要使用定时器来模拟requestAnimationFrame

59. 谈谈你对webpack的看法

  • 我当时使用webpack的一个最主要的原因是为了简化页面依赖的管理,并且通过将其打包为一个文件来降低页面加载时请求的资源数

  • 我认为webpack的主要原理是,它将所有的资源都看成一个模块,并且把页面逻辑当作一个整体,通过一个给定的入口文件,webpack从这个入口文件开始,找到所有的依赖文件,将各个依赖文件模块通过loader和plugin处理后,打包在一起,最后输出一个浏览器可识别的js文件

  • webpack有四个核心概念,分别是entry(入口)、output(输出)、loader和plugin

    • entry 是webpack的入口起点,它指示webpack应该从哪个模块开始着手,来作为其构建内部依赖图的开始
    • output 属性告诉webpack在哪里输出它所创建的打包文件,也可指定打包文件的名称
    • loader 可以理解为webpack的编译器,它使得webpack可以处理一些非js文件,
    • plugin 插件可以用于执行范围更广的任务,包括打包、优化、压缩等
  • 使用webpack的确能够提供我们对于项目的管理,但是它的缺点就是调试和配置比较复杂。webpack高版本的免配置在一定程度上解决了这个问题,但是我感觉,就是一个黑盒,很多时候出现了问题,没有办法很好的定位。

60. 异步编程的方式

  • 使用回调函数的方式,使用回调函数的缺点是多个回调函数嵌套的时候会造成回调函数地狱 ,回调函数间的代码耦合度太高,不利于代码的可维护
  • promise方式,使用promise的方式可以将嵌套的回调函数作为链式调用。
  • generator方式,可以在函数的执行过程中,将函数的执行权转移出去,在函数外部我们还可以将执行权转移回来。当我们遇到异步函数执行时,将异步函数的执行权转移出去,当异步函数执行完毕的时候我们再将执行权给转移回来。因此在generator内部对于异步操作的方式,可以以同步的顺序来书写
  • async方式,async函数是generator和promise实现的一个自动执行的语法糖,它内部自带执行器,当函数内部执行到一个await语句的时候,返回一个promise对象,那么函数将会等待promise对象的状态变为resolve后再继续向下执行。因此我们可以将异步的逻辑,转化为同步的顺序来书写,并且这个函数可以自动执行。

61. get请求传参长度的误区

  • 我们经常说get请求参数的大小存在限制,而post请求的参数大小是无限制的
  • 实际上http协议从未规定get/post的请求长度限制是多少,对get请求参数的限制是来源于与浏览器或者web服务器,浏览器或web服务器限制了url长度,
    • http协议未规定get和post的长度限制
    • get的最大长度限制是因为浏览器和web服务器限制了url的长度
    • 不同浏览器和web服务器,限制的长度大小不一致

62. Set、weakSet、map、weakMap

  • set,类似于数组,但是成员都是唯一的,没有重复值
  • weakSet,与set相似,也是不重复的值的集合,但是weakSet的成员只能是对象,而不是其他类型的值,weakSet中的对象都是弱引用,即垃圾回收机制不考虑weakSet对该对象的引用
  • map,类似于对象,也是键值对的集合,但是键名的范围不限于字符串,各种类型的值都可以当作键
  • weakMap,与map相似,也是用于生成键值对的集合,但是weakMap只接收对象作为键名,不接受其他类型的值作为键名,而且weakMap的键名指向的对象,不计入垃圾回收机制

63. 什么是proxy

  • proxy用于修改某些操作的默认行为,等同于在语言层面作出修改
  • proxy可以理解成,在目标对象之前架设一层拦截,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和修改,

64. 单例模式是什么

单例模式保证了全局只有一个实例来被访问,比如说常用的如弹框组件的实现和全局状态的实现

65. vue的生命周期

  • vue的生命周期指的是组件从创建到销毁的一系列的过程,被称为vue的生命周期。通过提供的vue在生命周期各个阶段的钩子函数,我们可以很好的在vue的各个生命阶段实现一些操作
    • beforeCreate钩子函数,在实例初始化之后,在数据监听和事件配置之前触发
    • created钩子函数,在实例创建完成后触发,此时可以访问data属性,但这个时候组件还没有被挂载到页面中,一般我们可以在这个阶段执行一些页面初始化的操作
    • beforeMount钩子函数,在组件被挂载到页面之前触发
    • mounted钩子函数,在组件挂载之后触发,此时可以通过domapi获取到页面的dom元素
    • beforeUpdate钩子函数,在响应式数据更新时触发,发送在虚拟dom重新渲染和打补丁之前,此时可以移除监听事件
    • updated 钩子函数,虚拟dom重新渲染和打补丁之后凋也
    • beforeDestory钩子函数,在实例销毁前调用,可以销毁定时器,解绑全局事件
    • destoryed 钩子函数,实例销毁后调用
    • 当我们使用keep-alive时,还有actived和deactived。用keep-alive包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行deactived钩子函数,命中缓存渲染后会执行actived钩子函数

66. vue组件间的参数传递

  • 父子组件间通信
    • 子组件通过props来接受父组件的数据,然后父组件在自组件上注册监听事件,自组件通过emit触发事件来向父组件传递数据
    • 通过ref属性给自组件设置一个名字,父组件通过ref组件名来获得子组件,子组件通过ref组件名来获得子组件,子组件通过parent获得父组件,可以实现通信
    • 使用provider/inject,在父组件中通过provider提供变量,在子组件中通过inject来将变量注册到组件中,不论子组件有多深,只要调用了inject就可以注入provider中的变量
  • 兄弟组件间通信
    • 事业eventBus,它的本质是通过创建一个空的vue实例来作为消息传递的对象,通信的组件引入这个实例,通信的组件通过在这个实例上监听和触发事件,来进行消息的传递
  • 如果业务逻辑复杂,很多组件之间需要同时处理一些公共的数据,这个时候采用上面方法不利于项目的维护。这个时候可以使用vuex,vuex的思想就是将这些公共的数据抽离出来,将它作为一个全局的变量来管理,然后其它组件就可以对这个公共数据进行读写操作,达到解耦的目的

67. computed和watch的差异

  • computed 是计算一个新的属性,并将其挂载到vue实例上,而watch是监听已经存在且已经挂载到vue实例上的数据。
  • cpmputed本质是一个惰性求值的观察者,具有缓存性,只有依赖发生变化后,第一次访问computed属性才会计算新值。而watch则是数据发生变化便会立即调用执行函数
  • 从使用场景来说,computed适用于一个数据被多个数据影响,而watch适用于一个数据影响多个数据

68. routeroute和router的区别

  • $route是路由信息对象,包括path,params,hash,query,fullpath,matched,name等路由信息参数
  • $router是路由实例对象,包括了路由的跳转方法,钩子函数等

69. 如何判断一个对象是否为空对象

function checkNullObj(obj) {
    return Object.keys(obj).length === 0 && Object.getOwnPropertySymbols(obj).length === 0
}

70. 手写观察者模式

// 手写观察者模式
var events = (function() {
    var topics = {}
    return {
        // 注册监听函数
        subscribe: function (topic, handler) {
            if (!topics.hasOwnProperty(topic)) {
                topics[topic] = []
            }
            topics[topic].push(handler)
        },

        // 发布事件,触发观察者回调事件
        publish: function (topic, info) {
            if (topics.hasOwnProperty(topic)) {
                topics[topic].forEach(handler => {
                    handler(info)
                })
            }
        },

        // 移除主题的一个观察者的回调事件
        remove: function (topic, handler) {
            if (!topics.hasOwnProperty(topic)) return
            var handleIndex = -1
            topics[topic].forEach((item, index) => {
                if (item === handler) {
                    handleIndex = index
                }
            })
            if (handleIndex >= 0) {
                topics[topic].splice(handleIndex, 1)
            }
        },

        // 移除主题的所有观察者的回调事件
        removeAll: function (topic) {
            if (topics.hasOwnProperty(topic)) {
                topics[topic] = []
            }
        }
    }
})()

71. 手写eventemitter

class EventEmitter {
    constructor() {
        this.events = {}
    }
    on(event, callback) {
        let callbacks = this.events[event] || []
        callbacks.push(callback)
        this.events[event] = callbacks
    }
    off(event, callback) {
        let callbacks = this.events[event]
        this.events[event] = callbacks && callbacks.filter(fn => fn !== callback)
    }
    emit(event, ...args) {
        let callbacks = this.events[event]
        callbacks.forEach(fn => {
            fn(...args)
        })
        return this
    }
    once(event, callback) {
        let wrapFun = (...args) => {
            callback(...args)
            this.off(event, wrapFun)
        }
        this.on(event, wrapFun)
        return this
    }
}

72. post和get的区别

  • get请求用于对服务器不会产生影响的场景,比如请求一个网页。而post请求一般用于对服务器资源会产生影响的情景,比如注册用户这一类操作
  • get请求将请求参数放入url中向服务器发送,这样的做法相对于post请求来说,一个方面是不太安全,因为请求的url会被保留在历史记录中,并且浏览器对于url长度上有限制,所以还会影响到get请求发送数据的长度

73. TLS/SSL为什么一定要三个随机数来生成会话密钥

客户端和服务端都需要生成随机数,以此保证每次生成的密钥都不相同。使用三个随机数,是因为ssl的协议默认不信任每个主机都能产生完全随机数,如果只使用一个伪随机的数来生成密钥,就很容易被破解。通过使用三个随机数的方式,增加了自由度,一个伪随机数可能被破解,三个伪随机数就很接近于随机了,因此可以使用这种方式来保证生成密钥的随机性和安全性

74. SSL连接断开后如何恢复

  • 一共有两种方法来恢复断开的ssl连接,一种是使用session ID,一种是session ticket
  • 使用session ID的方式,每一个会话都有一个编号,当对话中断后,下一次重新连接时,只要客户端给出这个编号,服务端如果有这个编号的记录,那么双方就可以继续使用以前的秘钥,而不用重新生成。目前所有的浏览器都支持这种方式,这种方式有个缺点,session ID只能够存在一台服务器上,如果我们的请求通过负载均衡被转移到了其他的服务器上,那么就无法恢复对话
  • 使用session ticket方式,session ticket时服务器在上一次对话中发送给客户的,这个ticket是加密的,只有服务器能够解密,其中包含了本次会话的信息,比如对话密钥和加密方法等,这样不管我们的请求是否转移到其他服务器上,当服务器将ticket解密后,就能够获取上次对话的信息,就不用重新生成对话密钥了

75. 当你在浏览器中输入google.com并按下回车会发生什么

  • 首先会对url进行解析,分析所需要使用的传输协议和请求的资源路径。如果输入的url中的协议或者主机名不合法,将会把地址栏中的内容传递给搜索引擎。如果没有问题,浏览器会检查url中是否出现了非法字符,如果存在非法字符,则对非法字符进行转义后再进行下一过程。
  • 浏览器会判断所请求的资源是否在缓存里,如果请求的资源在缓存中并且没有失效,那么就直接使用,否则向浏览器发起请求
  • 下一步我们首先需要获取的是输入的url中的玉米的ip地址,首先会判断本地是否有该域名的ip地址缓存,如果有则使用,如果没有则向本地dns服务器发起请求。本地dns服务器也会先检查是否存在缓存,如果没有就会先向根域名服务器发起请求,获得负责的顶级域名服务器的地址后,再向顶级域名服务器请求,然后获取负责的权威域名服务器的地址,再向权威域名服务器发起请求,最终获得域名的ip地址后,本地dns服务器再将这个ip地址返回给请求的用户
  • 下面是tcp建立连接的三次握手的过程,首先客户端先向服务端发送一个SYN连接请求报文段和一个随机序号,服务端接收到请求后向客户端发送一个消息包,ACK标志位为1,表示确认客户端发起的第一次连接请求,当客户端收到服务端的确认响应报文后,继续给服务端进行回应,也是一个ack标志位,服务端接收到确认后,也进入连接建立状态,此时双方的连接就建立起来了
  • 如果是使用的https协议,在通信前还存在一个tls的握手过程。
    • 首先由客户端向服务端发送使用的协议的版本号、一个随机数和可以使用的加密套件列表。
    • 服务端收到后,确认加密方法,也向客户端发送一个随机数和自己的数字证书
    • 客户端收到后,先检查数字证书是否有效,如果有效,则再生成一个随机数,并使用数字证书中的公钥对随机数进行加密,然后发送给服务端,
    • 服务端接收到后,使用自己的私钥对数据进行解密,拿到随机数
    • 这时候双方都有了三个随机数,按照之前所约定的加密方法,使用这三个随机数生成一把密钥,以后双方通信前,就使用这个秘钥对数据进行加密后再传输
  • 当页面请求发送到服务端后,服务端会返回一个html文件作为响应,浏览器接收到响应后,开始对html文件进行解析,开始页面渲染
  • 浏览器首先根据html文件构建dom树,根据解析到的css文件构建cssom树,然后利用dom树和cssom树构建render树,渲染树构建好后,会根据渲染树进行布局,布局完成后,最后使用浏览器的ui接口对页面进行绘制,这个时候整个页面就显示出来了
  • 最后一步是tcp断开连接的四次挥手过程
    • 客户端会先发送一个报文,在报文里FIN标志位为1,当服务端接收到这个报文后,知道客户端想和我断开连接。但是服务端可能还有消息未发送完成,还需要继续发送。
    • 服务端会先发送一个消息确认的响应,告诉客户端,我已经接收到你的断开请求,然后继续发送未发送完全的消息
    • 待消息发送完后,会发送一个FIN=1的报文给客户端,表示服务端已经做好断开连接的准备,客户端收到这个消息,会发送给服务端一个确认消息的报文,经过这四次挥手,不管是服务端还是客户端都已经做好了断开连接的准备,于是连接就可以断开了

76. cdn服务

cdn是一个内容分发网络,通过对网站资源的缓存,利用本身多台位于不同地域、不同运营商的服务器,向用户提供就近访问的功能,也就是说,用户的请求并不是直接发送给源网站,而是发送给cdn服务器,由cdn服务器将请求定位到最近的含有该资源的服务器上去请求,这样有利于提高网站的访问速度,同时通过这种方式也减轻了源服务器的访问压力

77. 正向代理和反向代理

  • 正向代理,它隐藏了真实的请求客户端,服务器不知道真实的客户端是谁,客户端请求的服务,都被代理服务器代替来请求
  • 反向代理,隐藏了真实的服务端,当我们请求一个网站的时候,背后可能有多台服务器为我们服务,但具体是哪一台,我们不需要关心,我们只需要知道反向代理服务器就可以了,反向代理服务器会帮我们将请求转发到真实的服务器那里去,反向代理服务器一般用来实现负载均衡

78. 负载均衡的两种实现方式

  • 一种是使用反向代理的方式,用户的请求都发送到反向代理服务器上,然后由反向代理服务器来转发到真实的服务器上,以此来实现集群的负载均衡
  • 另一种是dns的方式,dns可以用于在冗余的服务器上实现负载均衡,因为现在一般的大型网站使用多台服务器提供服务,因此一个域名可能会对应多个服务器地址。当用户向网站域名请求的时候,dns服务器返回这个域名所对应的服务器ip地址的集合,但在每个回答中,会循环这些ip地址的顺序,用户一般会选择排在前面的地址发送请求,以此将用户的请求均衡的分配到各个不同的服务器上,这样来实现负载均衡

89. 及时通讯的实现,短轮询、长轮询和websocket间的区别

  • 短轮询的基本思路是浏览器每隔一段时间向服务器发送http请求,服务器接收到请求后,不论是否有数据更新,都直接响应,这种方式本质上还是浏览器发送请求,服务器响应,通过让客户端不断的进行请求,使得客户端能够模拟实时地接收到服务端的数据变化,这种方式需要不断的建立http连接,严重浪费了服务端和客户端的资源,当用户增加时,服务端的压力就会变大,这是很不合理的
  • 长轮询的基本思路是,首先由客户端向服务端发起请求,当服务器接收到客户端发起的请求后,服务端不会立即响应,而是将这个请求挂起,然后判断服务器端数据是否有更新,如果有更新,则进行响应。如果一直没有数据,则到达一定的时间限制才返回。客户端js响应处理函数会在处理完服务器返回的信息后,再次发起请求,重新建立连接
  • websocker协议,websocker是http5定义的一个新协议,与传统的http协议不同,该协议允许由服务器主动的向客户端推送消息,websocker是一个全双工协议,也就是说通信双方是同等的,可以相互发送消息,

90. 如何实现多个网站之间共享登录状态

  • 在多个网站之间共享登录状态指的就是单点登录,多个应用系统总,用户只需要登录一次就可以访问所有相互信任的应用系统
  • 我认为单点登录可以这样实现,首先将用户验证中心单独独立出来,作为一个单独的验证中心,该验证中心的作用是判断客户端发送的账号密码的正确性,然后向客户端返回对应的用户信息,并且返回一个由服务器端秘钥加密的登录信息的token给客户,token具有一定的时效性。当一个应用系统跳到另外一个应用系统时,通过url参数的方式传递token,然后转移到应用站点发送给认证中心,认证中心对token进行解密后验证,如果用户信息没有过期,则向客户端返回对应的用户信息,如果失效了则会将页面重定向回单点登录页面