2021前端面试知识点总结之js篇|8月更文挑战

1,084 阅读8分钟

这是我参与8月更文挑战的第1天,活动详情查看:8月更文挑战

写在前面

面试,每个职场人必不可少的经历,对于研发人员来说可能相对更频繁一点(此处无任何感情色彩)。都说不打无准备的仗,特别是对于我们技术人员来说更是如此,面试前必须得好好准备准备,你,准备好了吗?

我是小憨憨,一个持续性学习,不间断写bug的前端工程师

js数据类型

  • 基本类型:Number String Boolean Null Undefined Symbol BigInt

  • 引用类型:Object

数据类型判断

  • typeof: 返回值有number string boolean undefined symbol bigint object function
typeof null    // object  'JavaScript 诞生以来便如此'
typeof NaN     // number
  • instanceof: 基于原型链的判断,可检测出引用类型。
  • constructor: 不安全,constructor的指向可以被改变。null和undefined没有此方法。
  • Object.prototype.toString.call(): 可以精确的判断出数据类型,返回值为[object 首字母大写的数据类型]

声明提升

  • 函数提升优先级比变量提升要高,且不会被变量声明覆盖,但是会被变量赋值覆盖,也会被后面的同名函数替换。
console.log(typeof(foo));   //function
function foo(){}
var foo = 5;
console.log(foo) // 5

let 和 const

  • 不存在变量提升。
  • 暂时性死区:只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。
  • 不允许重复声明。
  • 为js新增了块级作用域。
  • const实际上保证的,并不是变量的值不得改动,而是变量指向的那个 内存地址 所保存的数据不得改动。
const obj = {}
// 给obj添加一个prop属性,可以成功
obj.prop = 123
console.log(obj.prop) // 123

// 将obj指向另一个对象,会报错
obj = {}

js对象的深浅拷贝

  • 浅拷贝: 只是复制对象的引用。 ps: 只能拷贝一层是指,当对象的属性值为引用类型时,其嵌套的深层不能被拷贝

Object.assgin({}, targetObj): 只能拷贝一层

...运算符:只能拷贝一层

JSON.parse(JSON.stringify(obj)): 只能拷贝一层。当值为函数、undefined、或symbol时,无法拷贝。 当拷贝具有循环引用的对象时,报错。

arr.slice(): 只能拷贝一层

arr.concat(): 只能拷贝一层

  • 深拷贝: 才是对对象的真正的拷贝。
function deepClone(obj) {
  if(typeof obj !== 'object'){
    return obj
  }
  const newObj = Array.isArray(obj) ? [] : {};
  for(let key in obj){
    if(obj.hasOwnProperty(key)){
      newObj[key] = typeof obj[key] === 'object' ? deepClone(obj[key]) : obj[key]
    }
  }
  return newObj
}

this

  • this是当前执行代码的环境对象,在非严格模式下总是指向一个对象,严格模式下可以是任意值。
  • 在全局执行环境中this指向全局对象。
  • 在函数内部,this的值取决于函数被调用的方式。
  • this不能在函数执行期间被赋值,并且在每次函数被调用时this的值也可能会不同。
  • this 总是指向最后一次调用它的对象。

改变this指向的操作

  • call, apply, bind
  • 箭头函数(箭头函数没有自己的this,会从作用域的上一层继承this)
  • new 当一个函数被用作构造函数时,他的this被绑定到了正在构造的新对象上。 ps: new的绑定优先级要高于bind

call, bind, apply

  • call: 立即调用,第二个参数为单个的。
  • apply:立即调用,第二个参数是数组。
  • bind: 不会立即调用,返回一个新的函数,并且永久绑定this。(也就意味着一经bind绑定的this不可再次修改this指向)。

手写实现

  • call 实现的核心思想:调用call之后会立即执行,我们将call方法中的this作为call方法第一个参数(对象)的属性,然后执行这个属性并给其传入后续参数,删除这个属性。
Function.prototype.call = function (context = window, ...args) {
  // 声明独一无二的fnSymbol
  const fnSymbol = Symbol("fn");
  
  context[fnSymbol] = this;
  // 执行方法
  context[fnSymbol](...args);
  // 删除fnSymbol属性
  delete context[fnSymbol];
}
  • apply 实现思想跟call差不多,直接上代码。唯一不同的是apply第二个参数必须是数组。
Function.prototype.apply = function (context = window, argsArr) {
  const fnSymbol = Symbol("fn");
  context[fnSymbol] = this;
  context[fnSymbol](...argsArr);
  delete context[fnSymbol];
}
  • bind 核心思想:不会立即执行,而是返回一个函数
Function.prototype.bind = function (context = window, ...args) {
  const fnSymbol = Symbol("fn");
  context[fnSymbol] = this;
  
  return function (..._args) {
    // 组装所有的参数
    args = args.concat(_args);
    
    context[fnSymbol](...args);
    delete context[fnSymbol];   
  }
}

箭头函数

  • 箭头函数写法更简单;
  • 箭头函数没有自己的this,只会从作用域链的上一层继承this,并且是在定义的时候就确定并固定了。;
  • call,bind,apply不可更改箭头函数的this指向;
  • 箭头函数不可以使用yield命令,不能作为generator函数;
  • 箭头函数没有自己的arguments,在箭头函数中访问arguments实际上获取的是外层局部(函数)执行环境中的值;
  • 箭头函数没有原型prototype;
  • 不能使用new。

作用域

  • 作用域是一套规则,用来管理引擎如何在当前作用域以及嵌套子作用域中根据标识符名称进行变量查找。
  • 当我们访问一个变量时,编译器在执行这段代码时,会先从当前的作用域中查找是否有这个标识符,如果没有找到,就会去父作用域中查找,如果父作用域中没有找到就会继续向上查找,直到全局作用域为止。而在这个查找过程中形成的一个链条,就称之为作用域链。

闭包

  • 闭包是一种现象,在js中由于有顶层对象的存在,可以说是每当创建一个函数的时候,闭包就会在函数创建的同时被一同创建出来。
  • 闭包让你可以在一个内层函数中访问到其外层函数的作用域。
  • 闭包产生的本质是:当前环境中存在对父级作用域的引用。
  • 闭包的表现形式: 1. 返回一个函数; 2. 作为函数参数传递; 3. 在定时器、事件监听、Ajax请求、跨窗口通信、Web Workers或者任何异步中,只要使用了回调函数,实际上就是在使用闭包; 4. IIFE(立即执行函数表达式)创建闭包, 保存了全局作用域window和当前函数的作用域,因此可以全局的变量。
var a = 1;
function foo(){
  var a = 2;
  function baz(){
    console.log(a);
  }
  bar(baz);
}
function bar(fn){
  // 这就是闭包
  fn();
}
// 输出2
foo();


// 定时器
setTimeout(function timeHandler(){
  console.log('111');
},100)

// 事件监听
$('#app').click(function(){
  console.log('DOM Listener');
})

var a = 2;
(function IIFE(){
  // 输出2
  console.log(a);
})();

原型链

  • 每个实例对象都有一个私有的__proto__属性,指向它的构造函数的原型对象(prototype)。
  • 当对象查找一个属性的时候,如果自身没有找到,那么就会查找自身的原型对象,如果原型还没有找到,那么就会继续查找原型对象的原型,直到找到Object.prototype的原型时,此时原型为null,停止查找。这种通过原型链接的逐级向上的查找链被称为原型链。

new关键字

new 做了三件事:

  • 创建了一个新对象并返回;
  • 将新对象的__proto__属性指向了构造函数的原型对象;
  • 执行了构造函数中的代码(为新对象添加属性)。
// 手写实现new
function _new(fn, ...arg) {
  // 创建新对象将其原型对象指向构造函数的原型对象
  const obj = Object.create(fn.prototype);
  // 将this指向新创建的对象并指向该构造函数
  const ret = fn.apply(obj, arg);
  // 如果构造函数的执行结果是Object类型则返回执行结果,否则返回新创建的对象
  return ret instanceof Object ? ret : obj;
}

面试题:

function Constructor() {
    this.a = "123";
    // 第一种情况
    return false
    //第二种情况
    return []  或者 return {} 或者 return () => {}
    // 第三种情况
    没有return
} 
var obj = new Constructor();

类数组

  • 类数组是具有length属性的对象。典型的代表就是arguments
  • 类数组转为数组:
Array.from(arguments);

Array.prototype.slice.apply(arguments);

[].slice.apply(arguments);

[...arguments]
// ...扩展运算符,只能作用于 `iterable` 对象,即拥有 `Symbol(Symbol.iterator)` 属性值

js继承

原型链继承

其本质是将一个对象的原型对象指向另一个对象的实例对象。

  • 优点:多个实例可以共享原型链上定义的属性和方法。
  • 缺点:每个实例对引用类型的属性的修改也会被其他实例共享,这不是我们想看到的。
  • 缺点:子类在实例化的时候不能给父类构造函数传参。

原型链继承详情查看

构造函数继承

实现原理是:在子类的构造函数中,通过apply或者call的形式,调用父类构造函数,以实现继承。

  • 缺点:实例并不是父类的实例,仅仅只是子类的实例。
  • 缺点:子类无法继承父类原型链上的方法。
  • 缺点:每次生成子类实例都会执行一次父函数。

组合继承

实现思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。

构造函数以及组合继承详情查看

寄生组合式继承

  • 实现思路: 不必为了指定子类型的原型而调用父类的构造函数,我们所需要的无非就是父类原型的一个副本而已
  • 本质上,就是使用寄生式继承来继承父类的原型,然后再将结果指定给子类的原型。 寄生组合式继承详情查看

Class的extends继承

常见的设计模式

  • 工厂模式
  • 构造函数模式
  • 原型模式
  • 构造函数+原型的混合模式
  • 观察者模式
  • 发布订阅模式

Promise

  • 是一个构造函数,用来生成Promise的实例。
  • 简单说Promise就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作的)结果。
  • promise对象的状态不受外界影响,只能从pedding变为fullied或者rejected。
  • 一旦状态改变,就不会再变,任何时候都可以得到这个结果。
  • 无法取消promise
  • 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部
  • 处于pedding状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

实现一个promise版本的ajax?

实现思路简单整理如下:

  1. 首先我们要知道发起一个网络请求应该是_ajax(url, params)这样子的,即必须有请求地址 请求参数(可选)
  2. 而promise是可以链式的调用then方法的,那么要想链式调用then方法_ajax的返回值必须是一个promise对象 此时我们大体的架子已经有了
function _ajax(url, params){
    const promise = new Promise();
    return promise;
}
  1. 接下来我们去丰富里边的内容:
  2. new 一个promise,接收一个函数作为参数
  3. 该函数接收两个参数,resolve和reject,它们是两个函数,由js引擎提供
function _ajax(url, params){
    function fn(resolve, reject){
        
    }
    const promise = new Promise(fn);
    return promise;
}
  1. 接着创建xhr对象 即new XMLHttpRequest()
  2. 然后绑定事件onreadystatechange 在xhr的readyState属性发生变化的时候做出处理
  3. 接着.open()打开连接,同时设置请求方式,请求地址,以及是异步还是同步请求
  4. 然后可以使用setRequestHeader(k, v)添加http请求头字段
  5. 然后.send(params)发送请求
function _ajax(url, params){
    function fn(resolve, reject){
        const xhr = new XMLHttpRequest();
        const handleChange = function (){
          
        }
        xhr.onreadystatechange = handleChange;
        xhr.responseType = "json";
        xhr.setRequestHeader("Accept", 'application/json');
        xhr.open('POST', url);
        xhr.send(params);
    }
    const promise = new Promise(fn);
    return promise;
}
  1. 总的基本完成了 那么再去详细的完善 事件处理函数onreadystatechange
  2. readyState状态为:
    • 0: 尚未开始初始化,也没有调用open
    • 1: 此时调用了open但没有调用send
    • 2: 此时调用了send,但是服务器没有给出响应
    • 3: 此时正在接收服务器响应,但还没有接收完毕,此处一般不做任何处理
    • 4: 此时已经接收完了服务器的响应,可以对数据进行处理
  3. status状态码为200的时候进行处理
function _ajax(url, params){
  params = JSON.stringify(params);
  function fn(resolve, reject){
    const xhr = new XMLHttpRequest();
    const handleChange = function (){
      if(this.status === 200 && this.readyState === 4){
        resolve(this.response)
      } else {
        reject(new Error(this.statusText))
      }
    }
    xhr.onreadystatechange = handleChange;
    xhr.responseType = "json";
    xhr.setRequestHeader("Accept", 'application/json');
    xhr.open('POST', url);
    xhr.send(params);
  }
  const promise = new Promise(fn);

  return promise;
} 
  1. 至此,我们基于promise的ajax已经完成了
  2. 完美!

Promise.all()

  • 用于将多个Promise 实例,包装成一个新的Promise实例。
  • 接收一个数组作为参数。
  • 参数 数组中的元素 必须是 Promise实例,如果不是,会将其先转为Promise实例,再进行处理。
  • Promise.all()的参数也可以不是数组,但是必须有Iterator接口,且返回的每个成员都是Promise实例。
// 生成一个Promise对象的数组
const promises = [2, 3, 5, 7, 11, 13].map(function (id) {
  return getJSON('/post/' + id + ".json");
});

Promise.all(promises).then(function (posts) {
  // ...
}).catch(function(reason){
  // ...
});
  • 只有当 Promise.all()参数数组中的所有状态都变成fulfilled,或者其中有一个变为rejected,才会执行Promise.all()后面的.then等回调函数。
  • 注意,如果作为参数的 Promise 实例,自己定义了catch方法,那么它一旦被rejected,并不会触发Promise.all()catch方法。

实现Promise.all()

核心思想:

  • 传入参数为一个数组,如果是空的可迭代对象,则直接进行resolve
  • 如果参数重有一个promise失败,则返回的promise对象失败
  • 在任何情况下,Promise.all返回的promise的完成状态的结果都是一个数组
Promise.all = function(promises) {
    return new Promise(resolve, reject) {
        let result = [];
        let index = 0;
        let len = promises.length;
        if(len === 0) {
            return resolve(result)
        }
        for (let i = 0; i < len; i++) {
            Promise.resolve(promises[i]).then((data) => {
                result[i] = data;
                index++;
                if(index === len) resolve(result) 
            }).catch((err) => {
                reject(err)
            })
        }
    }
}

async await

  • async就是Generator函数的语法糖,使得异步操作更为便捷。
  • async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。
  • async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。
  • async函数的await命令后面,可以是 Promise对象 和 原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。
  • async函数返回一个Promise对象
  • async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。
  • async函数内部return语句返回的值,会成为then方法回调函数的参数。
  • async函数内部抛出错误,会导致返回的 Promise 对象变为reject状态。抛出的错误对象会被catch方法回调函数接收到。
  • await命令后面是一个Promise对象,则返回的就是promise对象成功的结果。如果不是Promise对象,则直接返回对应的值
async function test() {
   // 使用try catch 避免后续代码执行不下去
   try {
       const res1 = await ajax请求;
   } catch {
       // 错误时执行的代码
   }
   const res2 = await ajax请求;
   
}

async await 执行时机

let a = 0
let b = async () => {
    a = a + await 10
    console.log('2', a) // -> '2' 10
}
b()
a++
console.log('1', a) // -> '1' 1

上述代码中:

  • 首先b函数执行,
  • 执行到await之前变量a还是0,先将await后面的表达式执行一边(即 10),而 await内部实现了generator,generator会保留堆栈中的东西,所以此时 a = 0 被保存了下来。
  • 然后将后续的代码(a = 0 + 10)加入微任务队列,
  • 接着跳出async函数,执行后面的同步代码,此时a++ 输出了 1,
  • 然后同步代码全部执行完毕,再将微任务队列中的待执行代码压入执行栈执行,此时会将保存下来的值拿出来使用 a = 0 + 10

下面看个变种:

let a = 0
let b = async () => {
    let a1 = await 10
    a = a + a1
    console.log('2', a) // -> '2' 11
}
b()
a++
console.log('1', a) // -> '1' 1

js事件循环机制

js是单线程的,其内的任务分为同步任务和异步任务。

  • 同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。
  • 异步任务:不进入主线程,而进入 任务队列 的任务。只有 任务队列 通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
  • 主线程的所有同步任务全部执行完毕之后,才会执行任务队列中的异步任务。
  • 任务队列又分为:宏任务队列和微任务队列。
  • 宏任务包括:setTimeout setInterval setImmediate(node独有) requestAnimationFrame(浏览器独有) UIrendering(浏览器独有) I/O
  • 微任务包括:process.nextTick, MutationObserver, Promise.then catch finally
  • 微任务会优先于宏任务执行。
  • new Promise()是同步任务,会立即执行。
function fn(){
    console.log(1);
    
    setTimeout(() => {
        console.log(2);
        Promise.resolve().then(() => {
            console.log(3);
        });
    },0);
    
    new Promise((resolve, reject) => {
        console.log(4);
        resolve(5);
    }).then(data => {
        console.log(data);
    });
    
    setTimeout(() => {
        console.log(6);
    },0);
    
    console.log(7);
}
fn(); 
 
/** 附上 详细的代码执行过程:
1. 首先自上而下执行代码,得到此时的 
    stack: [console.log(1), new Promise(), console.log(7)];
    macrotask: [setTimeout1, setTimeout2]
    microtask: [promise2.then]
    此时的输出为:1, 4, 7
2. 到此全部的同步任务执行完毕,调用栈清空;
    stack: [];
    macrotask: [setTimeout1, setTimeout2]
    microtask: [promise2.then]
3. 将微任务压入执行栈,执行
    stack: [promise2.then];
    macrotask: [setTimeout1, setTimeout2]
    microtask: []
    输出 5
4. 此时,微任务队列中为空,故此开始执行宏任务,将宏任务队列中的setTimeout1压入执行栈;
    stack: [setTimeout1];
    macrotask: [setTimeout2]
    microtask: []
    输出 2,
    在执行setTimeout1时 又创建了一个微任务,放入microtask中
    stack: []
    macrotask: [setTimeout2]
    microtask: [promise1.then]
5. 将新创建的微任务压入执行栈执行
    stack: [promise1.then]
    macrotask: [setTimeout2]
    microtask: []
    输出:3
6. 到此,微任务执行完毕,开始执行宏任务
    stack: [setTimeout2]
    macrotask: []
    microtask: []
    输出:3
7. 最后,执行栈和任务队列全部为空,代码执行完毕。
    最终输出顺序为:1 4 7 5 2 3 6
 
完美!!!
**/

防抖和节流

防抖

  • 顾名思义,防抖就是防止抖动。那为什么会防抖呢,必然是因为持续性抖动,造成了不好的影响。就好比你单手拿着手机拍照,手臂总是不听使唤的不自觉微抖就会影响最终拍摄效果,那么防抖支架也就应运而生。我们程序中的防抖功能也是因此而生,直白点讲防抖就是:不管触发多少次,函数总是在最后一次触发之后的 n 秒后执行 一次

多用于:input输入框,减少资源请求;以及window的resize

// 参数fn为需要执行的回调函数,delay为每次推迟执行的延迟时间
function debounce(fn, delay) {
    // 创建定时器
    let timer = null;
    
    return function() {
        // 每次执行fn之前先判断timer是否存在,如果存在清除掉,以便万一有下一次的话,重新开始计时
        if (timer) {
            clearTimeout(timer)
        }
        timer = setTimeout(() => {
            fn.apply(this, [...argments])
        }, delay)
    }
}

节流

  • 顾名思义,就是节省流量。好比水管子,粗的和细的,同一时间内这俩水管子的流量是不一样的。在我们程序中就是,当持续触发事件时,保证一定时间段内只调用一次事件处理函数。

多用于:鼠标的mousedown事件;监听滚动事件等。

// 参数fn是需要执行的事件处理函数,interval是时间间隔的阀值
function throttle(fn, interval) {
    // 定义上一次触发回调事件的时间
    let last = 0;
    return function () {
        // 记录当前时间
        let now = +new Date();
        // 如果当前时间和上一次的时间 差值 大于等于 时间阀值 则执行一次事件处理函数
        if (now - last >= interval) {
            last = now
            fn.apply(this, [...arguments]);
        }
    }
}

重绘和回流

重绘

  • 当render tree中的一些元素需要更新属性,而这些属性只是影响元素的外观、风格,而不影响布局的,就叫重绘。(比如:background-color)

回流也叫重排

  • 当render tree中的一部分(或全部),因为元素的规模尺寸、布局、隐藏等改变而需要重新构建的,就叫回流。
  • 每个页面至少需要一次回流,即页面第一次渲染的时候,这时是一定会发生回流的。因为需要构建render tree。
  • 页面布局和几何属性改变时就需要回流。
  • 回流必将引起重绘,重绘不一定会引起回流。

触发重绘的属性

  • color, background-color, visibility, opacity
  • border-style, border-radius
  • box-shandow, outline 等

触发回流的情况

  • DOM元素的几何属性发生变化时会触发回流。(width, height, padding, margin, border
  • DOM元素的移动、删除、增加会触发回流。
  • 读写位置属性时:offset, scroll, client等属性会触发回流。
  • 调用window.getComputedStyle也会触发回流。

如何减少重绘和回流

  • 减少样式操作。
  • 减少DOM操作
  • 避免频繁的直接访问计算后的样式,而是应该先将信息保存下来。(比方说获取dom节点的属性信息,如下代码:)
for(var i = 0;i<=10;i++){
    // 应该像这样先将拿到的dom属性信息保存在一个变量中
   var body = document.getElementsByTagName('body')[0]
   var h = body.offsetHeight;
}
  • 新建图层:将频繁重绘和回流的dom元素单独作为一个独立图层,因此缩小了重绘和回流的影响范围。(比如,虚拟DOM)
  • 绝对布局的DOM,不会造成大量回流。
  • html不要嵌套太深,否则会加大对页面布局的计算消耗。

前端缓存

缓存的优点

  • 减少了冗余的数据传递,节省宽带流量。
  • 减少了服务器负担,大大提高了网站性能。
  • 加快了客户端加载网页的速度,这也正是http缓存属于客户端缓存的原因。

强缓存

  • 定义:当缓存库中存在所请求的数据时,直接使用缓存库中的数据,没有才去服务器请求。
  • 强缓存主要是通过http请求头中的Cache-ControlExpire两个字段控制。
  • 通常会设置Cache-Control的值为 public, max-age=xxx,表示在xxx秒内再次访问该资源,均使用本地的缓存,不再向服务器发起请求。
  • Expire:值为服务端返回的数据到期时间。(再次请求的时间小于返回到期时间时,会使用缓存数据)一般不用处理此值。

协商缓存

  • 客户端会先从缓存库中获取一个缓存数据的标识,拿到标识后请求服务器验证是否有效,如果有效,则服务器会返回304,此时直接使用缓存数据。若失效,则服务端会返回更新后的数据。
  • Last-Modified:服务器在响应请求时,会告诉浏览器资源的最后修改时间。
  • Etag:服务器响应请求时,通过此字段告诉浏览器当前资源在服务器生成的唯一标识(生成规则由服务器决定)。

cookie

  • 数据始终在同源的http请求中携带,即在浏览器和服务器之间来回传递。
  • 数据有路径的概念,可以限制cookie只在某个路径下。
  • 存储大小只有4kb
  • 只在设置的过期时间之前有效,跟浏览器关闭与否无关。
  • cookie 可以在http头部设置set-cookie属性: httponly属性可以防止xss攻击,它会禁止js脚本访问cookie; source属性告诉浏览器仅在https的时候发送cookie

sessionStorage

  • 数据保存在本地,不会自动将数据发送给服务器。
  • 数据仅在当前浏览器窗口关闭前有效,不可持久化数据。
  • 在所有的同源窗口中都是共享的。
  • 对存储的数据大小有限制。(500KB左右的东西保存起来就会令到Resources变卡,2M左右就可以令到Resources卡死,操作不了,5M就到了Chrome的极限,而超过之后就会抛出异常。 DOMException: Failed to execute 'setItem' on 'Storage': Setting the value of 'widgetCacheData' exceeded the quota.

localStorage

  • 数据保存在本地,不会自动将数据发送给服务器。
  • 数据始终有效,窗口和浏览器关闭也一直保存,可做 数据持久化。
  • 在所有的同源窗口中都是共享的。
  • 对存储的数据大小有限制。(500KB左右的东西保存起来就会令到Resources变卡,2M左右就可以令到Resources卡死,操作不了,5M就到了Chrome的极限,而超过之后就会抛出异常。 DOMException: Failed to execute 'setItem' on 'Storage': Setting the value of 'widgetCacheData' exceeded the quota.

indexedDB

  • 是一种底层 API,用于在客户端存储大量的结构化数据(也包括文件/二进制大型对象(blobs))。

  • 该 API 使用索引实现对数据的高性能搜索。

  • IndexedDB 是一个事务型数据库系统,类似于基于 SQL 的 RDBMS。

  • IndexedDB 是一个基于 JavaScript 的面向对象数据库。indexedDB 允许您存储和检索用索引的对象;可以存储结构化克隆算法支持的任何对象。您只需要指定数据库模式,打开与数据库的连接,然后检索和更新一系列事务

  • 使用 IndexedDB 执行的操作是异步执行的,以免阻塞应用程序。

  • 为了获取数据库的访问权限: 需要在 window 对象的 indexedDB 属性上调用 open() 方法。该方法返回一个IDBRequest对象;异步操作通过在IDBRequest对象上触发事件来和调用程序进行通信。

  • 具体使用方法,可以自行百度,此处不做更多的赘述。

fetch,ajax,axios

fetch

  • fetch: 号称是ajax的替代品,是基于Promise设计的API,是原生js。
  • fetch 可以接受第二个可选参数,一个可以控制不同配置的init对象。
  • fetch 不能开箱即用。
  • fetch 不支持超时控制。
  • fetch 与 jQuery.ajax()有三点不同:
  1. 当接收到一个代表错误的HTTP状态码时,从 fetch()返回的 Promise 不会被标记为reject,即使响应的HTTP状态码是404或500 。 相反,它会将Promise状态标记为 resolve(但是会将resolve的返回值的ok属性设置为false)仅当网络故障时或者请求被阻止时,才会被标记为 reject
  2. fetch() 可以接受跨域cookies,你也可以使用fetch()建立起跨域会话。
  3. fetch 不会发送cookies。除非你使用了credentials 的初始化选项

ajax

  • ajax是对原生XHR的封装,支持JSONP。

axios

  • axios是一个基于Promise用于浏览器和nodejs的HTTP客户端,本质上也是对原生XHR的封装,只不过是Promise的实现版本,符合最新的ES规范。
  • 可以在nodejs中使用。
  • 提供了并发请求的接口。
  • 支持Promise API。

http 相关

http是什么?

  • http 也就是超文本传输协议,是一个在计算机世界里专门在两点之间传输文字、图片、音频、视频等超文本数据的约定和规范。

http的优缺点?

  • 灵活可扩展。
  • 请求答应模式
  • 可靠传输
  • 无状态(即是优点也是缺点)
  • 明文传输
  • 队头阻塞

TCP/IP协议(互联网的关键技术)

  • TCP:传输控制协议;负责应用程序之间的通信
  • IP:网际协议;负责计算机之间的通信
  • TCP/IP协议族按层次分为四层:
  1. 应用层:规定了向用户提供应用服务时通信的协议。
  2. 传输层:对接上层应用层,提供处于网络连接中两台计算机之间的数据传输所使用的协议。
  3. 网络层:规定了数据通过怎样的传输路线到达对方计算机传送给对方(IP协议等)。
  4. 链路层:用来处理连接网络的硬件部分,包括控制操作系统、硬件的设备驱动、NIC(Network Interface Card,网络适配器,即网卡),及光纤等物理可见部分(还包括连接器等一切传输媒介)。硬件上的范畴均在链路层的作用范围之内。

URI

  • URI:统一资源标识符
  • URL:统一资源定位符
  • URN:统一资源名称

http请求方式

  • GET:通常用来获取资源
  • POST:通常用来提交数据
  • PUT:通常用来修改数据
  • DELETE:删除数据(一般不用)
  • HEAD:获取资源的元信息
  • CONNECT:建立连接通道,用于代理服务器
  • OPTIONS:列出可对资源实行的请求方法,用来跨域请求
  • TRACE:追踪请求-响应的传输路径

get和post的区别

  • get在url上显式的传参,并且有数据长度限制,而post隐式的传参,无数据长度限制。
  • get请求返回的数据会被浏览器缓存起来,而post请求的返回数据不会被缓存
  • get对数据进行查询,post进行增删改
  • get相对于post来说不安全,会暴露信息,但是实际上两者都是不安全的,因为http协议式明文传输,要想安全需使用https

部分http状态码

  • 1xx:表示目前是协议处理的中间状态,还需后续操作。
  • 2xx:表示成功状态。
  • 3xx:重定向状态,资源位置发生变动,需要重新请求。
  • 4xx:请求报文有误。
  • 5xx:服务端发生错误。
状态码描述
200成功,表示从客户端发来的请求在服务端被正确处理。
204No content,即没有内容,表示没有资源返回,即不包含主体数据
206Partail Content 进行范围请求成功
301moved permanently,永久性重定向,表示资源已被分配了新的 URL
302found,临时性重定向,表示资源临时被分配了新的 URL
303see other,表示资源存在着另一个 URL,应使用 GET 方法获取资源
304not modified,当协商缓存命中时会返回这个状态码(与重定向无关)
400bad request,请求报文存在语法错误
401unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息
403forbidden,表示对请求资源的访问被服务器拒绝,可在实体主体部分返回原因描述
404not found,表示在服务器上没有找到请求的资源
500internal sever error,表示服务器端在执行请求时发生了错误
501Not Implemented,表示客户端请求的功能还不支持
502服务器自身是正常的,但访问的时候出错了,具体是啥错误不知道。
503Service Unavailable 表示服务器当前很忙,暂时无法响应服务

跨域

  • 跨域是由于浏览器的同源策略导致的。
  • 当一个请求url的协议、域名、端口三者之中的任意一个与当前页面url不同时即为跨域。
  • 跨域解决办法
  1. CORS即跨域资源共享
  2. JSONP:利用script标签的src实现跨域请求拿到响应,只支持get方法。
  3. Nginx反向代理
  4. Proxy
  5. postMessage
  6. window.name
  7. document.domain

三次握手和四次挥手

  • 第一次握手:客户端给服务器发送一个 SYN 报文。
  • 第二次握手:服务器收到 SYN 报文之后,会应答一个 SYN+ACK 报文。
  • 第三次握手:客户端收到 SYN+ACK 报文之后,会回应一个 ACK 报文。
  • 服务器收到 ACK 报文之后,三次握手建立完成。
  • 第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于 FIN_WAIT1状态。
  • 第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 + 1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于CLOSE_WAIT状态。
  • 第三次挥手:客户端收到服务端的 ACK 之后,进入FIN_WAIT2状态,如果服务端也想断开连接了,会向客户端发送 FIN 报文,且指定一个序列号。此时服务端处于LAST_ACK的状态。
  • 第四次挥手:客户端收到 FIN 之后,回复一个 ACK 报文作为应答,且把服务端的序列号值 + 1 作为自己 ACK 报文的序列号值,此时客户端处于TIME_WAIT状态。服务端收到 ACK 之后,进入CLOSED状态,至此服务端已经完成了连接的关闭。

详细请查看:跟着动画来学习TCP三次握手和四次挥手

https

  • 超文本传输安全协议。
  • HTTPS经由HTTP进行通信,但利用SSL/TLS来加密数据包。HTTPS开发的主要目的,是提供对网站服务器的身份认证,保护交换数据的隐私与完整性。

从输入url到页面渲染结束发生了什么?

  • 浏览器的地址栏输入url并按下回车;
  • 浏览器查找当前url是否存在缓存,并比较缓存是否果期;
  • DNS解析url对应的ip;
  • 根据ip建立TCP连接(三次握手);
  • http发起请求;
  • 服务端处理请求,浏览器接受响应;
  • 渲染页面,构建DOM树;
  • 关闭TCP连接(四次挥手)

什么是babel?

  • babel是一个js转译器,用于在当前和旧浏览器或环境中将es2015+代码转换为向后兼容的js版本,主要功能有:
  • 转换语法;
  • 目标环境中缺少的polyfill功能
  • 源代码转换

js事件流

  • 事件流描述的是从页面中接收事件的顺序。
  • IE的事件流是事件冒泡流
  • Netscape Communicator的事件流是事件捕获流

事件冒泡

  • 事件开始时由最具体的元素接收,然后逐级向上传播到较为不具体的节点

事件捕获

  • 事件捕获的思想是不太具体的节点应该更早接收到事件,而最具体的节点应该最后接收到事件。
  • 事件捕获的用意在于在事件到达预定目标之前捕获它。

DOM事件流

  • DOM2事件流包括三个阶段:事件捕获阶段处于目标阶段事件冒泡阶段

eval是做什么的?

  • eval() 函数会将传入的字符串当做 JavaScript 代码进行执行。
  • eval()函数是全局对象的一个函数属性。
  • 参数是一个表示 JavaScript 表达式、语句或一系列语句的字符串。
  • 返回值:返回字符串中代码的返回值。如果返回值为空,则返回 undefined。
  • 如果 eval() 的参数不是字符串, eval() 会将参数原封不动地返回。
  • 应该避免使用eval,不安全,非常耗性能(2次,一次解析成js语句,一次执行)。
  • 由JSON字符串转换为JSON对象的时候可以用eval,var obj =eval('('+ str +')');

手写系列

实现一个new

/**
核心思想:
1.  创建了一个新对象并返回
2.  将新对象的__proto__属性指向了构造函数的原型对象
3.  执行了构造函数中的代码(为新对象添加属性)
*/
function _new(fn, ...arg) {
  // 创建新对象将其原型对象指向构造函数的原型对象
  const obj = Object.create(fn.prototype);
  // 将this指向新创建的对象并指向该构造函数
  const ret = fn.apply(obj, arg);
  // 如果构造函数的执行结果是Object类型则返回执行结果,否则返回新创建的对象
  return ret instanceof Object ? ret : obj;
}

看个栗子:

function Constructor() {
    this.a = "123";
    // 第一种情况
    return false
    //第二种情况
    return []  或者 return {} 或者 return () => {}
    // 第三种情况
    没有return
} 
var obj = new Constructor();

事件总线(发布订阅模式)

class EventEmitter {
    constructor() {
        this.cache = {}
    }
    on(name, fn) {
        if (this.cache[name]) {
            this.cache[name].push(fn)
        } else {
            this.cache[name] = [fn]
        }
    }
    off(name, fn) {
        let tasks = this.cache[name]
        if (tasks) {
            const index = tasks.findIndex(f => f === fn || f.callback === fn)
            if (index >= 0) {
                tasks.splice(index, 1)
            }
        }
    }
    emit(name, once = false, ...args) {
        if (this.cache[name]) {
            // 创建副本,如果回调函数内继续注册相同事件,会造成死循环
            let tasks = this.cache[name].slice()
            for (let fn of tasks) {
                fn(...args)
            }
            if (once) {
                delete this.cache[name]
            }
        }
    }
}

// 测试
let eventBus = new EventEmitter()
let fn1 = function(name, age) {
	console.log(`${name} ${age}`)
}
let fn2 = function(name, age) {
	console.log(`hello, ${name} ${age}`)
}
eventBus.on('aaa', fn1)
eventBus.on('aaa', fn2)
eventBus.emit('aaa', false, '布兰', 12)
// '布兰 12'
// 'hello, 布兰 12'

函数柯里化

  • 函数柯里化就是将使用多个参数的函数转换成一系列使用一个参数的函数的技术。 看🌰:
function add(a, b, c) {
    return a + b + c
}
add(1, 2, 3)
let addCurry = curry(add)
addCurry(1)(2)(3)

实现 curry 函数:

function curry(fn) {
    let judge = (...args) => {
        if (args.length === fn.length) {
            return fn(...args)
        }
        return (...arg) => judge(...args, ...arg)
    }
    return judge
}

实现instanceof

// 核心是原型链的向上查找
function instanceof(left, right) {
    // 如果是基本类型 直接返回false
    if(typeof left !== 'object' || left === null) {
        return false
    }
    // 获取左侧参数的原型对象
    let proto = Object.getPrototypeOf(left)
    while(true) {
        // 找到原型链顶端还未找到,则返回false
        if(proto === null){
            return false
        }
        if (proto === right.prototype) {
            return true
        }
        // 迭代
        proto = Object.getPrototypeOf(proto)
    }
}

手写系列更详细内容,请移步:

不完全面试题

['1', '2', '3'].map(parseInt) 返回值是啥?

  • parseInt(string, [radix]): 接收两个参数
    • string(必需):要被解析的字符串。
    • radix(可选):要解析的数字的基数,介于2~36之间。
      • 如果省略该参数或其值为0,则以10为基数来解析;
      • 如果以'0x'或'0X'开头,则以16为基数来解析;
      • 如果其值小于2或者大于36,则parseInt()返回NaN。
  • 第一个非空格字符不能转换为数字时,parseInt()返回NaN。

如何让if(a == 1 && a == 2)条件成立

let a = {
    value: 0,
    valueOf() {
        this.value++;
        return this.value
    }
}

递归实现 1 到 n 的整数之和

function count(n) {
    if (n === 1) {
        return 1
    }
    return count(n - 1) + n
}
let sum = count(100);
console.log('和:', sum);

综合性考察很强的一道面试题

function Foo () {
    getName = function () { alert(1) }
    return this
}
Foo.getName = function () { alert(2) }
  
Foo.prototype.getName = function () { alert(3) }

var getName = function () { alert(4) }

function getName () { alert(5) }


Foo.getName(); // 2
getName(); // 4
Foo().getName(); // 1
getName(); // 1
new Foo.getName(); // 2
new Foo().getName(); // 3 
new new Foo().getName(); // 3

欲知详细解答,请移步:一道被前端忽略的基础题,不信看你会几题

写在最后

本系列文章旨在记录前端知识点,不会太详细的展开讲述,初期可能整理的不是很全面,后续会不定期持续补充完善。同时,也欢迎大家评论补充,若有错误的地方也请评论指出,我会及时更正。最后,欢迎一键三连。

我是小憨憨,关注我不迷路,一起学习,一起进步。