web前端知识点(JavaScript篇)

call,apply,bind

call,apply,bind这三者的区别,及内部实现原理,点这里

promise

promise函数的内部实现原理,点这里

闭包

闭包就是能够读取其他函数内部变量的函数。形式上,就是一个函数返回一个内部函数到函数外,内部函数引用外部函数的局部变量。本质上,闭包是将函数内部和函数外部连接起来的桥梁。

原型链

JavaScript中每一个对象都有一个__proto__和constructor属性,每一个函数都有一个prototype属性,因函数也是对象,所以函数也拥有__proto__和constructor属性。
__proto__指向的是它们的原型对象,也可以理解为父对象。如果访问本身一个不存在的属性,那么没有获取之后会去它的原型对象去获取,而原型对象本身也是一个普通对象,如果在它的原型对象中同样没有获取到,那么就会往原型对象的原型对象去获取,直到顶层对象null(原型链终点,一个没有任何属性的对象),返回undefined。这就形成了一条原型链。
prototype属性是函数独有的,是从一个函数指向一个对象,称之为函数的原型对象。原型对象内包含特定类型所有实例共享的属性和方法,作用为被该函数实例化出来的对象找到共用的属性和方法。
constructor是从一个对象指向一个函数,称之为该对象的构造函数。每个对象都有对应的构造函数,因为对象的建立前提是需要有constructor。

节流与防抖

节流:
节流是在规定的时间内只执行一次,稀释函数执行频率。比如规定时间2s内执行了一次函数,那么在这2s内再次触发将不会执行。

function throttle(time, fn) {
  let isRun = false
  return function () {
    if (isRun) return
    isRun = true
    let arg = [...arguments]
    setTimeout(() => {
      fn.apply(null, arg)
      isRun = false
    }, time * 1000)
  }
}

防抖:
防抖是在等待的时间内不断触发函数,但函数真正执行的将是最后触发的那次。比如规定时间为2s,如果第二次与第一次的触发的时间间隔小于2s,那么第一次将会被清除,留第二次触发的函数继续等待,如果2s内没有第三次触发,将执行第二次触发的函数,如果2s内又触发了第三次,那么第二次触发的函数也将被清除,留第三次触发的函数继续等待。

function debounce(time, fn) {
  let timer = null
  return function () {
    let arg = [...arguments]
    if (timer) {
      clearTimeout(timer)
    }
    timer = setTimeout(() => {
      fn.apply(null, arg)
      clearTimeout(timer)
      timer = null
    }, time * 1000)
  }
}

斐波那契数列、快排、冒泡排序

斐波那契数列:1、1、2、3、5、8、13、21、……

// 递归
function fibonacci(num) {
  if (num === 1 || num === 2) {
    return 1
  }
  return fibonacci(num - 2) + fibonacci(num - 1)
}
// 循环
function fibonacci1(n) {
  var n1 = 1, n2 = 1, sum;
  for (let i = 2; i < n; i++) {
    sum = n1 + n2
    n1 = n2
    n2 = sum
  }
  return sum
}

快速排序:

function quickSortFn(_arr) {
  let arr = [..._arr]
  if (arr.length <= 1) {
    return arr
  }
  let left = []
  let right = []
  let item = arr.pop()
  for (let i = 0, len = arr.length; i = item) {
      right.push(val)
    } else {
      left.push(val)
    }
  }
  return [...quickSortFn(left), item, ...quickSortFn(right)]
}

冒泡排序:

function bubbleSort(_arr) {
  let arr = [..._arr]
  let len = arr.length
  for (let i = 0; i < len - 1; i++) {
    for (let k = i + 1; k  arr[k]) {
        [arr[i], arr[k]] = [arr[k], arr[i]]
      }
    }
  }
  return arr
}

多维数组转一维数组

// 第一种
let a = [1,2,[3,4],[5,[6,[7,8]],9]]
a.join(',').split(',')

// 第二种
function unid1(arr) {
  for (let item of arr) {
    if (Object.prototype.toString.call(item).slice(8, -1) === 'Array') {
      unid1(item);
    } else {
      result.push(item);
    }
  }
  return result;
}

js流程控制

function LazyMan(name) {
  this.task = []
  let self = this
  let fn = (name => {
    return () => {
      console.log(name)
      self.next()
    }
  })(name)
  self.task.push(fn)
  setTimeout(() => {
    console.log(222)
    self.next()
  })
}
LazyMan.prototype = {
  constructor: LazyMan,
  next() {
    let fn = this.task.shift()
    fn && fn()
  },
  eat(val) {
    let self = this
    self.task.push((val => {
      return () => {
        console.log(val)
        self.next()
      }
    })(val))
    return this
  },
  sleep(num) {
    let self = this
    self.task.push((num => {
      return () => {
        setTimeout(() => {
          console.log(num)
          self.next()
        }, +num * 1000)
      }
    })(num))
    return this
  }
}
function lazyMan(name) {
  return new LazyMan(name)
}

lazyMan('zz').eat('lunch').sleep('3').eat('dinner')

对象深拷贝与浅拷贝

深拷贝与浅拷贝的区别本质是被复制出来的值的内存地址是否有改变,内存地址没变就是浅拷贝,有变就是深拷贝。这里涉及到了JavaScript的引用数据类型,引用数据类型的复制,复制的不是对象本身,而是一个指向该对象的指针,当这个对象本身的值改变,那么所有引用这个对象的变量都会改变。
浅拷贝:
Object.assign()
深拷贝:
JSON.parse(JSON.stringify(object)):
这个能够拷贝除Function、RegExp与undefined等类型之外的值,如果遇到这种类型,将会被自动忽略。
循环递归拷贝:

function getType(val) {
  return Object.prototype.toString.call(val).slice(8, -1)
}
function deepClone(obj) {
  if (obj && typeof obj === 'object') {
    let returnObj = getType(obj) === 'Array' ? [] : {}
    let item = ''
    for (let key in obj) {
      item = obj[key]
      if (key === "__proto__") {
        continue;
      }
      if (getType(item) === 'Array' || getType(item) === 'Object') {
        returnObj[key] = deepClone(item)
      } else {
        returnObj[key] = item
      }
    }
    return returnObj
  }
}

异步与事件轮询机制

JavaScript语言的核心特点就是单线程,单线程的原因主要是对DOM的操作,多线程操作DOM会引起冲突。为了利用多核CPU的计算能力,HTML5提出了web worker标准,允许JavaScript创建多线程,且创建线程完全受主线程控制,且不得操作DOM。
js的异步是通过回调函数实现的,即任务队列。虽然js是单线程的,但浏览器的多线程的,则js的执行遇到异步任务都会调用浏览器的多线程去执行,当异步任务有了结果,则会将异步任务的回调函数放入异步任务队列。
任务队列分为两种:宏任务队列与微任务队列。
当js从上往下执行时,如遇到异步任务,浏览器则用其他线程去执行,当异步任务有了结果,则将回调函数放到任务队列中,当主执行栈执行完后,会去查询微任务队列,如果有则执行,微任务队列执行完后,则将宏任务队列放入主执行栈重新开始下一轮循环。
不同的js异步API的回调函数放入不同的任务队列。
宏任务(macrotask)队列API:

  • setTimeout
  • setInterval
  • setImmediate(node,IE10+)
  • requestAnimationFrame(浏览器)

微任务(microtask)队列API:

  • process.nextTick(node)
  • MutationObserver(浏览器)
  • Promise.then catch finally

注意的一点:微任务队列中的微任务回调函数是放入当前微任务队列中,而不是下轮循环队列。

浏览器垃圾回收机制

  • 标记清除

大部分浏览器以此方式进行垃圾回收,当变量进入执行环境(函数中声明变量,执行时)的时候,垃圾回收器将其标记为“进入环境”,当变量离开环境的时候(函数执行结束)将其标记为“离开环境”,在离开环境之后还有的变量则是需要被删除的变量。标记方式不定,可以是某个特殊位的反转或维护一个列表等。

  • 引用计数

另一种不太常见的垃圾回收策略是引用计数。引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。当这个引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其所占的内存空间给收回来。这样,垃圾收集器下次再运行时,它就会释放那些引用次数为0的值所占的内存。

js执行上下文和执行栈

该点的解释则是表明JavaScript程序内部的执行机制。
执行上下文,简而言之,就是当前JavaScript代码被解析和执行时所在环境的抽象概念,JavaScript任何代码都是在执行上下文中运行。
三种类型:

  • 全局执行上下文:不在任何函数内的代码都处于全局执行上下文,一个程序只能有一个全局执行上下文。做了两件事:1、创建了一个全局对象,浏览器则是window;2、将this指向这个全局对象。
  • 函数执行上下文:每个函数都有自己的执行上下文。调用函数时,都会为这个函数创建一个新的执行上下文,也只在函数被调用时才会被创建。一个程序内的函数执行上下文没有数量限制,每当一个函数执行上下文被创建,则会执行一系列操作。
  • eval函数执行上下文:不常用,略。

生命周期:

  • 创建:创建变量对象,创建作用域链,确定this指向(this的赋值是在执行的时候确定的)。
  • 执行:变量赋值,代码执行。
  • 回收:执行完成,执行上下文出栈,等待回收。

管理执行上下文:
所有的执行上下文采用的是栈结构来管理,遵循先进后出。全局JavaScript代码在浏览器执行时,实现创建一个全局执行上下文,压入执行栈的底端,每创建一个函数执行上下文,则把它压入执行栈的顶端,等待函数执行完,该函数的执行上下文出栈等待回收。
JavaScript解析引擎总是访问执行栈的顶端,当浏览器关闭,则全局执行上下文出栈。

url输入到页面显示之间的过程

  • 用户输入的url作DNS解析,获取IP地址
  • 建立TCP连接
  • 发送HTTP请求,获取html文件
  • 解析HTML文件,构建DOM树及CSSOM规则树,然后合并渲染树,绘制界面。
  • 发送HTTP获取HTML文件内其他资源。

new操作符中的执行过程

  • 创建一个新对象 newObject
  • 将新对象 newObject 的 __proto__ 指向原函数 fn 的 prototype
  • 执行原函数 result = fn.call(newObject)
  • 判断返回类型,如果是值就返回这个result,如果是引用类型,返回这个引用对象

async/await的实现原理

async/await的作用为阻塞异步执行任务,等待异步任务执行完返回,再执行下面任务,异步任务返回的是一个Promise对象。
实现原理为generator + yield + promise:generator自动执行且返回一个promise对象。

let test = function () {
  // ret 为一个Promise对象,因为ES6语法规定 async 函数的返回值必须是一个 promise 对象
  let ret = _asyncToGenerator(function* () {
    for (let i = 0; i  {
    let gen = genFn();
    function step(key, arg) {
      let info = {};
      try {
        info = gen[key](arg);
      } catch (error) {
        reject(error);
        return;
      }
      if (info.done) {
        resolve(info.value);
      } else {
        return Promise.resolve(info.value).then((v) => {
          return step('next', v);
        }, (error) => {
          return step('throw', error);
        });
      }
    }
    step('next');
  });
}

跨域问题的产生及解决方案与原理

跨域是指一个域下的文档或脚本试图去请求另一个域下的资源,这里跨域是广义的。
而狭义的跨域是指:当浏览器与服务器通信的两个地址的协议、域名、端口,这三者任意一个不同,都会导致跨域问题的产生,这是基于浏览器的同源策略限制。
限制的行为:

  • cookie,localstorage和IndexDB无法读取
  • DOM无法获取
  • Ajax请求不能发送

解决方案:

  • jsonp跨域通信:只能用于get请求,基于浏览器允许HTML标签加载不同域名下的静态资源,通过script动态加载一个带参网址实现跨域通信实现跨域。
  • postMessage跨域:postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一。
  • nginx代理:服务器端调用HTTP接口只是使用HTTP协议,不会执行JS脚本,不需要同源策略,也就不存在跨越问题。
  • 跨域资源共享(CORS):只服务端设置Access-Control-Allow-Origin即可,前端无须设置,若要带cookie请求:前后端都需要设置。
  • nodejs中间件代理跨域:node中间件实现跨域代理,原理大致与nginx相同,都是通过启一个代理服务器,实现数据的转发,也可以通过设置cookieDomainRewrite参数修改响应头中cookie中域名,实现当前域的cookie写入。
  • WebSocket协议跨域:WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。
  • document.domain + iframe跨域:此方案仅限主域相同,子域不同的跨域应用场景。实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域
  • location.hash + iframe跨域:a欲与b跨域相互通信,通过中间页c来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。
  • window.name + iframe跨域:window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。

正向代理与反向代理的区别:
正向代理与反向代理并没有形式上的区别,只是一个认知的问题。比如a请求b有跨域问题,正向代理与反向代理都可以通过中介c来实现,a -> c -> b -> c -> a这样完成了一次跨域通信,如果a请求c,知道c会去请求b再返回,则是一个正向代理,如果a不知道请求c,c最终去请求了b,那这就是一个反向代理。最终目的地址以IP为准。

es6新特性

  • 字符串扩展:includes、startsWith、endsWith等新API及模板字符串。
  • 对象扩展:keys、values、entries、assian等。
  • 数组扩展:find、findIndex、includes等。
  • 新的变量声明:let、const。
  • 解构表达式:数组解构与对象解构。
  • 函数优化:函数参数默认值、箭头函数、对象的函数属性简写。
  • 数组优化:map与reduce等API的增加。
  • Promise:异步微任务API的增加。
  • 新数据结构:set、map。
  • 模块化:export、import。
  • 二进制与八进制字面量:数字前面添加0o/0O和0b/0B可将其转化为二进制和八进制。
  • 类class:原型链的语法糖表现形式。
  • for…of/for…in:新的遍历方式。
  • async/await:同步异步任务。
  • Symbol:新的数据类型,表示独一无二的值,最大的用法是用来定义对象的唯一属性名。

未完待续……