天天看点

web前端知识点(JavaScript篇)

call,apply,bind

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

promise

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

闭包

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

原型链

JavaScript中每一个对象都有一个__proto__和constructor属性,每一个函数都有一个prototype属性,因函数也是对象,所以函数也拥有__proto__和constructor属性。

__proto__指向的是它们的原型对象,也可以理解为父对象。如果访问本身一个不存在的属性,那么没有获取之后会去它的原型对象去获取,而原型对象本身也是一个普通对象,如果在它的原型对象中同样没有获取到,那么就会往原型对象的原型对象去获取,直到顶层对象null(原型链终点,一个没有任何属性的对象),返回undefined。这就形成了一条原型链。

prototype属性是函数独有的,是从一个函数指向一个对象,称之为函数的原型对象。原型对象内包含特定类型所有实例共享的属性和方法,作用为被该函数实例化出来的对象找到共用的属性和方法。

constructor是从一个对象指向一个函数,称之为该对象的构造函数。每个对象都有对应的构造函数,因为对象的建立前提是需要有constructor。

JS 异步并发控制

js 异步并发控制一个很重要的场景就是大文件的分片上传,加上 http1.1 能够同时发送6个请求,将大文件分成多个片段进行并发请求,能够减少上传时间,并且能够进行断点续传与暂停上传等功能。

以下是异步并发控制大致逻辑:

function sendRequest(arr, max = 6, callback) {
  let i = 0 // 数组下标
  let fetchArr = [] // 正在执行的请求

  let toFetch = () => {
    // 如果异步任务都已开始执行,剩最后一组,则结束并发控制
    if (i === arr.length) {
      return Promise.resolve()
    }

    // 执行异步任务
    let it = fetch(arr[i++])
    // 添加异步事件的完成处理
    it.then(() => {
      fetchArr.splice(fetchArr.indexOf(it), 1)
    })
    fetchArr.push(it)

    let p = Promise.resolve()
    // 如果并发数达到最大数,则等其中一个异步任务完成再添加
    if (fetchArr.length >= max) {
      p = Promise.race(fetchArr)
    }

    // 执行递归
    return p.then(() => toFetch())
  }

  toFetch().then(() => 
    // 最后一组全部执行完再执行回调函数
    Promise.all(fetchArr).then(() => {
      callback()
    })
  )
}      

节流与防抖

节流:

节流是在规定的时间内只执行一次,稀释函数执行频率。比如规定时间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 < len; i++) {
    let val = arr[i]
    if (val >= 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 < len; k++) {
      if (arr[i] > 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 为引用类型则返回 result,不是则返回新对象 newObject
    function $new (fn) {
      var _obj = {}
      var _args = Array.prototype.slice.call(arguments, 1)
      _obj.__proto__ = fn.prototype
      var result = fn.call(_obj, ..._args)
      if (typeof result === \'object\' || typeof result === \'function\') {
        return result
      }
      return _obj
    }
    
    var fn = function (name, age) {
      this.name = name;
      this.age = age;
    }
    $new(fn, \'李四\', 20)      

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 < 10; i++) {
      let result = yield sleep(1000);
      console.log(result);
    }
  });
  return ret;
}();

// generator 自执行器
function _asyncToGenerator(genFn) {
  return new Promise((resolve, reject) => {
    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:新的数据类型,表示独一无二的值,最大的用法是用来定义对象的唯一属性名。

详情点这里。

 优雅降级与渐进增强

优雅降级:一开始就针对低版本浏览器进行构建页面,完成基本的功能,然后再针对高级浏览器进行效果、交互、追加功能达到更好的体验。

渐进增强:一开始就构建站点的完整功能,然后针对浏览器测试和修复。比如一开始使用 CSS3 的特性构建了一个应用,然后逐步针对各大浏览器进行 hack 使其可以在低版本浏览器上正常浏览。

优雅降级和渐进增强都关注于同一网站在不同设备里不同浏览器下的表现程度。关键的区别则在于它们各自关注于何处,以及这种关注如何影响工作的流程。

优雅降级观点认为应该针对那些***、最完善的浏览器来设计网站。而将那些被认为“过时”或有功能缺失的浏览器下的测试工作安排在开发周期的最后阶段,并把测试对象限定为主流浏览器(如 IE、Mozilla 等)的前一个版本。在这种设计范例下,旧版的浏览器被认为仅能提供“简陋却无妨 (poor, but passable)” 的浏览体验。你可以做一些小的调整来适应某个特定的浏览器。但由于它们并非我们所关注的焦点,因此除了修复较大的错误之外,其它的差异将被直接忽略。

渐进增强观点则认为应关注于内容本身。请注意其中的差别:我甚至连“浏览器”三个字都没提。内容是我们建立网站的诱因。有的网站展示它,有的则收集它,有的寻求,有的操作,还有的网站甚至会包含以上的种种,但相同点是它们全都涉及到内容。这使得渐进增强成为一种更为合理的设计范例。这也是它立即被 Yahoo! 所采纳并用以构建其“分级式浏览器支持 (Graded Browser Support)”策略的原因所在。

重排(回流)与重绘

这两者之间的关系:重绘不一定重排,而重排一定重绘。

重排:当渲染树的一部分必须更新并且节点的尺寸发生了变化,浏览器会使渲染树中受到影响的部分失效,并重新构造渲染树。

重绘:在一个元素的外观被改变所触发的浏览器行为,浏览器会根据元素的新属性重新绘制,使元素呈现新的外观。

引起的原因:

重排:

  • 页面第一次渲染 在页面发生首次渲染的时候,所有组件都要进行首次布局,这是开销最大的一次回流。
  • 浏览器窗口尺寸改变
  • 元素位置和尺寸发生改变的时候
  • 新增和删除可见元素
  • 内容发生改变(文字数量或图片大小等等)
  • 元素字体大小变化。
  • 激活CSS伪类(例如::hover)。
  • 设置style属性
  • 查询某些属性或调用某些方法。比如说:offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight

除此之外,当我们调用getComputedStyle方法,或者IE里的currentStyle时,也会触发回流,原理是一样的,都为求一个“即时性”和“准确性”。

重绘:

  • 当render tree中的一些元素需要更新属性,而这些属性只是影响元素的外观,风格,而不会影响布局的,比如visibility、outline、背景色等属性的改变。

优化:

  • 不要一条一条地修改 DOM 的样式。可以先定义好 css 的 class,然后修改 DOM 的 className。
  • 不要把 DOM 结点的属性值放在一个循环里当成循环里的变量。
  • 为动画的 HTML 元件使用 fixed 或 absoult 的 position,那么修改他们的 CSS 是不会 reflow 的。
  • 千万不要使用 table 布局。因为可能很小的一个小改动会造成整个 table 的重新布局。table及其内部元素除外,它可能需要多次计算才能确定好其在渲染树中节点的属性,通常要花3倍于同等元素的时间。这也是为什么我们要避免使用table做布局的一个原因。
  • 不要在布局信息改变的时候做查询(会导致渲染队列强制刷新) 
  • 获取能引起回流的元素属性值,应进行缓存

未完待续......