前端常考小知识

549 阅读15分钟

1 new 操作符具体做了什么、如何实现?

// 1.用new Object() 的方式新建了一个对象 obj
// 2. 取出第一个参数,就是我们要传入的构造函数。
//    此外因为 shift 会修改原数组,所以 arguments 会被去除第一个参数
// 3.将 obj 的原型指向构造函数,这样 obj 就可以访问到构造函数原型中的属性
// 4. 使用 apply,改变构造函数 this 的指向到新建的对象,这样 obj 就可以访问到构造函数中的属性
// 5.返回 obj
function objectFactory() {
    let obj = new Object();
    Constructor = [].shift.call(arguments);
    obj.__proto__ = Constructor.prototype;
    let ret = Constructor.apply(obj, arguments);
    
    return typeof ret === 'object' ? ret : obj;
}

// 执行
objectFactory(构造函数,初始化参数)

理解原型及原型链: cavszhouyou.top/JavaScript%…
模拟 new 的实现:github.com/mqyqingfeng…

2 js 延迟加载的几种方式

  1. defer:将脚本文件设置为延迟加载,浏览器会再开启一个线程去下载 js 脚本,同时继续解析 html 文档,解析完毕后再去执行已经下载好的 js 脚本。
  2. async:这个属性会使脚本异步加载,不会阻碍页面的解析过程。但是脚本加载完后,会立即执行,如果此时页面解析还没有结束,同样会造成阻塞。多个 async 脚本的执行顺序是不可预测的,一般不会按照代码顺序执行。
  3. 将 js 脚本放在最后加载:常见的优化 js 加载的方法
  4. 动态创建 dom 元素:监听页面的加载状态,当文档加载完成后再创建 script 标签
// windown.DOMContenLoaded: DOM 解析完毕之后触发,这时 DOM 解析完成,js 可以获取到 DOM 的引用,但是页面中的一些图片等资源还没有加载完成。
// window.load: 所有资源全部加载完成,包括图片、视频等资源。
(function() {
    if(window.attachEvent {
        window.attachEvent('load', asyncLoad);
    } else {
        window.addEventListener('load', asyncLoad);
    }
    
    var asyncLoad = function(){
        var ga = document.createElement('script');
        ga.type = 'text/javascript';
        ga.async = 'true';
        ga.src = 'xxxx';
        var s = document.getElementByTagName('script')[0];
        s.parentNode.insertBefore(ga, s);
    }
)()
以下提供一个通用的事件监听方法
const EventUtils = {
    // 添加监听
    addEvent: function(ele, type, handler) {
        if(ele.addEventListener) {
            ele.addEventListener(type, handler, false);
        } else if(ele.attachEvent) {
            ele.attachEvent('on' + type, handler);
        } else {
            element['on' + type] = handler;
        }
    },
    // 移除事件监听
    removeEvent:function(ele, type, handler) {
        if(ele.removeEventListener) {
            ele.removeEventListener(type, handler);
        } else if(ele.detachEvent) {
            ele.detachEvent('on' + type, handler);
        } else {
            ele['on + type'] = null;
        }
    },
    // 获取事件目标
    getTarget:function(event){
        return event.target || event.srcElement; // 兼容 IE
    },
    // 获取 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(evnet.preventDefault) {
            event.preventDefault();
        } else {
            event.returnValue = false;
        }
    }
}

3 事件委托是什么

1 基本概念

通俗来说,就是把一个元素的响应事件(click,mousedown...)的函数委托到另一个元素;
一般来说,会把一个或者一组元素的事件委托到它的父级或者更外层元素上,真正绑定事件的是外层元素,当事件响应到需要绑定的元素时,会通过事件冒泡机制触发外层绑定的事件,然后在外层元素上去执行函数。

2 优点

  • 减少内存消耗:给每个 item 都绑定事件,是非常消耗内存的,影响性能
  • 动态绑定事件:事件绑定在父级,子元素的增减不影响事件的绑定

4 什么是闭包

1 定义

闭包是指有权访问另一个函数作用域中的变量的函数。

2 创建方式

在一个函数 a 中创建另一个函数 b ,在 b 中可以访问到 a 中的局部变量。

3 用途

  • 使函数外部能访问到函数内部的变量。通过使用闭包,可以在外部调用闭包函数,从而在外部访问到函数内部的变量,可以用这种方法来创建私有变量。
  • 使已经结束运行的函数上下文中的变量继续留在内存中。因为闭包中还有对这些变量的引用,所有不会被回收。

4 例子

function createFunctions(){
    var result = new Array();

    for(var i=0; i < 10; i++){
        result[i] = function(){
            console.log(i);
        }
    }
     return result;
}

var result = createFunctions();

result[0](); // 9 
result[1](); // 9 
result[2](); // 9 
result[3](); // 9 
result[4](); // 9 
result[5](); // 9

以上并不符合我们的期待,原因是: 执行 console.log 时,首先 JavaScript 引擎回去寻找当前函数变量对象,当前函数变量对象找不到 i 值时,会根据作用域链向上查找,于是就在 createFunctions 中找到了 i,此时的 i 是叠加之后的 i,因此打印结果为 9。
需要注意的是:一般来说,createFunctions 函数执行完成后,其 createFunctionsAO 就应该销毁了,但是因为 resultContext 中还保留着对它的引用,那么在垃圾回收时,判断可以通过引用找到该对象,那么就不会被清除。
解决方案:

function createFunctions(){
    var result = new Array();

    for(var i=0; i < 10; i++){
        result[i] = (function(num){
            console.log(num);
        })(i);
    }
     return result;
}

var result = createFunctions();

5 Ajax是什么

是一种异步通信的方式,由 js 脚本向服务器发起 http 通信,获取数据,然后更新当前网页的部分数据,实现局部刷新。
具体来说,Ajax 包括以下几个步骤:

  1. 创建 XMLHttpRequest 对象,即创建一个异步调用对象
  2. 创建一个 http 请求,并指定请求的方法、URL 以及验证信息
  3. 设置相应 http 请求的状态变化的函数
  4. 发送 http 请求
  5. 获取异步调用的返回结果数据
  6. 使用 JavaScript 和 dom 实现局部刷新
let xhr = new XMLHttpReuqest();
xhr.open(GET, 'server_url', true);
xhr.onreadystatechange = function(){
    if(this.readyState != 4){
        return;
    }
    
    if(this.status == 200) {
        handle(this.response);
    } else {
        console.error(this.statusText);
    }
}
xhr.onerror = function(){
    console.error(this.statusText);
}

xhr.responseType = 'json';
xhr.sendRequestHeader('Accept', 'application/json');
xhr.send();

6 浏览器的缓存机制

1 定义

浏览器缓存机制指的是在一段时间内保存已接收到的 web 资源的一个副本,如果在资源的效时间内再次发送对该资源的请求,那么浏览器就会直接使用缓存的副本而不是向浏览器发送请求。
因此,使用 web 缓存可以极大的提高网页的打开速度、减少不必要的网络宽度消耗、降低服务器的压力、减少网络延迟。

2 分类

一般由服务器指定:

  • 强缓存策略
  • 协商缓存策略

2.1 强缓存策略

强缓存是利用 expires 或者 cache-control 这两个 http response header 实现的,它们都用来表示资源在客户端缓存的有效期。

  • expires 是较老的强缓存管理 header,由于它是服务器返回的一个绝对时间。在服务器时间和客户端时间相差较大时,缓存管理容易出现问题。
  • cache-control:是一个相对时间,在配置缓存时以秒为单位,用数值表示。

2.2 协商缓存策略

当浏览器对某个资源没有命中强缓存时,就会发送一个请求到服务器,验证协商缓存是否命中,如果命中,则响应体返回 http 的状态为304并且会带一个 Not Modified 的字符串。

参考:juejin.im/post/684490…

7 什么是同源策略?以及跨域问题

1 定义

一个 js 脚本在未经允许的情况下,不可以访问另一个域中的内容。
同源指的是:两个域的协议、域名、端口号必须一致

2 目的

主要是为了保护用户信息的安全。它只是一种对 js 脚本的限制,对于一般的 img 或者 script 脚本请求都不会有跨域限制,因为它们通过响应结果来进行可能出现安全问题的操作。

3 限制

  • 第一个是当前域下的 js 脚本不能访问其他域下的 cookie、localhost 和 indexDB
  • 第二个是当前域下的 js 脚本不能操作其他域下的 DOM 元素
  • 第三个是当前域下的 ajax 不能发送跨域请求

4 怎么解决跨域问题

按目的划分:

  1. 只是实现主域名下不同子域名的跨域操作
  • document.domain + iframe:两个页面都通过 js 强制设置 document.domain 为基础主域
  1. 解决不同跨域窗口间的通信问题
  • location.hash + iframe:在主页面动态的修改 iframe 窗口的 hash 值,然后在 iframe 窗口
  • window.name + iframe
  • postMessage
  1. 如解决 ajax 无法提交跨域请求的问题
  • jsonp
// jsonp 的缺点是只能发送 get 请求
<script>
    var script = document.createElement('script');
    script.type = 'text/javascript';
    // 传一个回调函数名给后端
    script.src= 'https://www.domain2.com:8080/login?user=admin&callbalck=handleCallbak';
    document.appendChild(script);
    function handleCallback(res) {
        console.log(res)
    }
</script>
  • 跨域资源共享(CORS)
  1. 非简单请求,浏览器会发一次预检请求来判断该域名是否存在服务器的白名单中,收到肯定回复后才会发起请求
  • ngnix 代理跨域
  • nodejs 中间件代理跨域
  • WebScoket 协议跨域 WebScoket protocol 是 HTML5 一种新的协议。它实现了浏览器与服务器全双工通信、允许跨域通讯,是 server push 技术的一种很好的实现。Scoket.io 很好的封装了 WebScoket 接口,提供了更简单灵活的接口,对不支持 WebScoket 的浏览器提供了向下兼容。

参考:
segmentfault.com/a/119000001…
cors:www.ruanyifeng.com/blog/2016/0…

8 什么是 Cookie

1 定义

cookie 是服务器提供的一种用于维护会话状态信息的数据,通过服务器发送到浏览器,并保存在本地。当下一次有同源请求时,浏览器会自动的将 cookie 添加到请求头中发送给服务器。每个 cookie 的 maxSize 是4kb,每个域名下的 cookie 数量最多为20个。

2 用途

  • 会话状态管理(用户登陆状态、购物车、游戏分数等)
  • 个性化设置(自定义设置、主题等)
  • 浏览器行为跟踪(跟踪分析用户行为等)

3 配置

服务器端:使用 Set-Cookie 的响应头部来配置 cookie 信息。
cookie 包括以下5个属性:

  • expires:失效时间,GMT 格式
  • domian:域名
  • path:路径
  • secure:规定了 cookie 只能在确保安全的情况下传输
  • HttpOnly:规定了这个 cookie 只能被服务器访问,不能被 js 脚本访问

参考:
HTTP Cookie:developer.mozilla.org/zh-CN/docs/…

4 cookies, session 和 webStroage 的区别及应用场景

cookies 和 session 都是用来跟踪浏览器用户身份的会话方式。
区别:

  • 保持状态:cookies 保存在客户端,session 保存在服务器端
  • 存储类型:cookies 只能存储字符串类型,session 通过类似 HashTable 的数据结构存储
  • 存储大小:单个 cookies 不超过 4kb,session 无大小限制
  • 安全性: cookies 不如 session

而 webstroage 的目的:

  1. 提供一种在 cookies 之外的存储路径
  2. 提供一种存储大量可跨会话的存储机制

HTML5的WebStorage提供了loacalStroage 和 sessionStroage 两种API,区别:

  • 生命周期:前者只有手动清除才会消失,而后者只有关闭标签页就会消失(刷新不会)
  • 存储大小:都是 5 MB
  • 存储内容类型:都只能存储字符串类型
  • 获取方式: window.localStroage, window.sessionStroage

9 模块化开发

9.1 是模块化开发

一个模块实现一个特定的功能的一组方法。
由于函数具有独立作用域的特点,最原始的写法是使用函数作为模块,但这种方式容易造成全局变量污染,且模块间没有联系。
之后,提出了对象写法。将函数作为一个对象的方法来实现,这样解决了直接使用函数作为模块的一些缺点,但是这样会暴露所有的模块成员,外部代码可以修改内部属性的值。 现在,最常用的是立即执行函数的写法。通过闭包实现模块私有作用域的建立,同时不会对全局作用域造成污染。

9.2 js的几种模块规范

  1. CommonJS:通过 require 来引入模块,通过 module.exports 定义模块的输出接口。这是服务端的解决方案,以同步的形式引入需要的模块,因为服务端的数据都存在磁盘,读取非常快,加载没有问题。但在浏览器端,加载数据需要发送网络请求,用这种方式就没那么合适了。
  2. AMD:采用异步加载的方式来加载模块,模块的加载不影响后面语句的执行,所有依赖这个模块的语句都定义在一个回调函数中,等模块加载完成,执行回调。require.js实现了 AMD 规范。
  3. CMD:和 AMD 一样是异步加载,区别在于模块定义时对依赖的处理不同、对依赖模块的执行时机不同 区别:
  4. import/export:es6 提出

参考
浅谈模块化开发:juejin.im/post/684490…

10 document.write 和 innerHtml 的区别

前者:write 的内容会代替整个文档流的内容,重写整个页面 后者:仅代替指定元素的内容,重写页面部分内容

11 DOM 操作

11.1 创建新节点

  1. createDocumentFragment(node)

fragment 是一个指向空DocumentFragment对象的引用。文档片段存在于内存中,并不在 DOM 树中,所以将子元素插入到文档片段时,不会引起页面的回流。
let fragment = document.createDocumentFragment()

  1. createElement(TagName[,options]):创建一个由标签名指定的 html 元素
  2. createTextNode(text):创建一个新的文本节点

11.2 添加、移除、替换、插入

  1. appendChild(node)
  2. removeChild(node)
  3. parentNode.replaceChild(new, old)
  4. parentNode .insertBefore(new:新节点, refrenceNode:将要插在这个结点之前)

11.3 查找

  1. getElementById()
  2. getElementByName()
  3. getElementByTagName()
  4. getElementByClassName()
  5. quetySelector()
  6. querySelectorAll()

11.4 属性操作

  1. getAttribute(key)
  2. setAttribute(key, value)
  3. hasAttribute(key)
  4. removeAttribute(key)

12 call() 和 apply()

12.1 区别

作用一模一样,仅传入的参数形式不同 apply(thisArg, [argsArray])
call(thisArg, arg1, arg2,...)

13 类数组

13.1 什么是类数组

一个拥有length属性和若干索引属性的对象,就可以被称为类数组对象。和数组相似,但是不能调用数组的方法。

13.2 常见的类数组

  1. arguments 方法的返回结果
  2. DOM 方法的返回结果

13.3 常用类数组转数组的方法

(1)通过 call 调用数组的 slice 方法来实现

Array.prototype.slice.call(arrayLike)

(2) 通过 call 调用数组的 splice 方法来实现转换

Array.prototype.splice.call(arrayLike, 0)

(3) 通过 apply 调用数组的 concat 方法来实现转换

Array.prototype.concat.apply([], arrayLike)

(4) 通过 Array.from 方法来实现转换

// 从一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例  
Array.from(arrayLike)

14 Javascript 作用域与变量声明提升

todo

15 垃圾回收

15.1 定义

有些数据使用后,可能不再需要,这种数据称为垃圾数据。 对垃圾数据进行回收,以释放有限的内存空间,称为垃圾回收。

15.2 类别

手动回收:何时分配、何时销毁内存都是由代码控制(c、c++)
自动回收:产生的垃圾是由垃圾回收器来释放,并不需要手动通过代码释放

15.3 调用栈中的数据如何回收

有一个记录当前执行状态的指针(称为ESP),指向调用栈中当前正在执行代码的上下文,执行完成后,JavaScript会将ESP下移到另一个执行上下文,这个下移的过程就是销毁只想上下文的过程。

15.4 堆中的数据如何回收

回收堆中数据需要用到JavaScript的垃圾回收器。
代际假说:两个特点

  • 大部分新生对象倾向于早死;
  • 不死对象,会活得更久。

15.4.1 以chrome中的v8引擎为例分析:

v8 中会把堆分为:

  • 新生代:存放生存时间短的对象(1~8M) —— 使用副垃圾回收器
  • 老生代:存放生存时间长的对象 —— 使用主垃圾回收器

垃圾回收器的流程:

  1. 标记空间中的活动对象和非活动对象
  2. 回收非活动对象所占据的内存
  3. 内存整理(频繁回收后存在的大量不连续空间,称为内存碎片)

副垃圾回收器
Scavenge算法:将新生代空间对半分为两个区域,一半是对象空间,一半是空闲区域。首先对对象区域中的垃圾做标记,副垃圾回收器会将存活的对象复制到空闲区域,并进行有序排列。完成后,进行对象区域和空闲区域角色翻转,这样就完成了垃圾对象的回收操作。

对象晋升策略:经过两次垃圾回收依然还存活的对象,会被移到老生区。

主垃圾回收器
老生区中的对象:占用空间大、对象存活时间长
采用 标记-清除 算法:

  1. 标记过程阶段:从一组根元素开始,递归遍历根元素,可到达为活动对象,否则为垃圾数据
  2. 垃圾清除:清除垃圾数据,然后使用 标记-整理算法,让所有存活对象向一端移动,然乎直接清理掉端边界以外的内存

标记清除、标记整理

15.5 全停顿

定义:JavaScript运行在主线程上,一旦执行垃圾回收算法会将其他正在执行的js脚本暂停,待垃圾回收完毕再恢复。这种行为,称为全停顿。
此时,使用增量标记法,V8将标记过程分为一个个的子标记过程,同时让垃圾回收标记和JavaScript应用逻辑交替进行,直到标记阶段完成。由此,可将完成的垃圾回收任务分为许多小的任务,在其他js任务中间穿插执行,避免用户感受到页面卡顿。

16 移动端点击事件延迟、点击穿透

16.1 延迟

原因: 因为移动端有双击缩放的操作,因此浏览器在 click 后会等待 300ms,看用户有没有下一次点击,来判断这次操作是不是双击。
解决方案:

  1. 通过 meta 标签禁用网页缩放
<meta name="viewport" content="user-scalable=no">
<meta name="viewport" content="initial-scale=1,maximum-scale=1">
  1. 通过 meta 标签将网页的 viewport 设置为 ideal viewpoint
  2. 调用一些 js 库,如 FastClick

16.2 穿透

假如页面上有两个元素A和B。B元素在A元素之上。我们在B元素的touchstart事件上注册了一个回调函数,该回调函数的作用是隐藏B元素。我们发现,当我们点击B元素,B元素被隐藏了,随后,A元素触发了click事件。
这是因为在移动端浏览器,事件执行的顺序是touchstart > touchend > click。而click事件有300ms的延迟,当touchstart事件把B元素隐藏之后,隔了300ms,浏览器触发了click事件,但是此时B元素不见了,所以该事件被派发到了A元素身上。如果A元素是一个链接,那此时页面就会意外地跳转。

转自:juejin.im/post/684490…

17 前端路由

17.1 定义

前端路由就是把不同路由对应不同的内容或页面交给前端来做,之前是后端根据不同的请求返回不同的页面内容。

17.2 何时使用

在单页应用,大部分页面结构不变,只改变部分内容

17.3 优缺点

优点:用户体验好,不需要每次都从服务器端请求页面数据,能快速展示
缺点:单页面无法在前进/后退时记住之前滚动的位置

17.4 实现方式

  • hash
  • pushState

18 事件循环

宏任务:

  1. 页面进程中存在消息队列和事件循环机制,渲染进程内部会维护多个消息队列,如延迟执行队列、普通的消息队列
  2. 宏任务:在消息队列中的任务称为宏任务(setTimeout)

微任务:

  1. 需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前
  2. 产生微任务的方式:
    • MutationObserver:监控dom节点,当dom节点发生变化时,就会产生 DOM 变化记录的微任务
    • Promise.resolve() Promise.reject()
  3. 在执行微任务过程中生成的新的微任务,也会加到微任务队列中,V8 引擎会一直循环执行,直到队列为空。也就是,新产生的微任务不会推迟到下个宏任务中执行,而是在当前宏任务中继续。

总结: 宏任务在任务队列中依次执行,每执行完成一个宏任务的主函数且宏任务未结束之前,执行该宏任务的微任务队列,微任务产生的新的微任务,继续在当前宏任务中执行,直到所有的微任务都结束,然后再执行下一个宏任务。