Giter Site home page Giter Site logo

inchill / fe-notes Goto Github PK

View Code? Open in Web Editor NEW
69.0 1.0 1.0 24.9 MB

🍎个人前端知识汇总,并且处于持续更新中。 This is a summary of personal front-end knowledges, and it will be updated from time to time.

JavaScript 34.60% HTML 65.40%
javascipt front-end

fe-notes's Introduction

Hi there 👋 My Visitor Count visitor!

Top Langs

fe-notes's People

Contributors

inchill avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

Forkers

shan-yu1

fe-notes's Issues

从 v-if 和 v-show 来看 display: none 和 visibility: hidden

display: none

DOM 元素在渲染时不会占用空间。将其从文档流中取出,并将其从可访问性树中删除,不会响应任何事件。

opacity: 0

更改元素透明度,仍然在文档流中,并且可响应事件。假如页面上有几个单/复选框,可通过 tab 键顺序切换。

visibility: hidden

更改元素的可见性,仍然在文档流中,不会响应任何事件。visibility: hidden 样式的行为类似于 opacity: 0 和 pointer-events: none 的组合。

JavaScript 引擎是如何工作的?

Chrome 浏览器 JavaScript 引擎执行代码主要分为了三个过程:

  1. 语法分析阶段:对代码进行语法分析,检查语法错误。
  2. 编译阶段:该阶段会进行执行上下文(Execution Context)的创建,包括创建变量对象、建立作用域链、确定 this 的指向等。每进入一个不同的运行环境时,V8 引擎都会创建一个新的执行上下文。
  3. 执行阶段:将编译阶段中创建的执行上下文压入调用栈,并成为正在运行的执行上下文,代码执行结束后,将其弹出调用栈。

重点看下编译阶段,这个阶段的核心是执行上下文的创建。

执行上下文的创建

执行上下文的创建离不开 JavaScript 的运行环境,JavaScript 运行环境包括全局环境、函数环境和 eval。

全局环境和函数环境的创建过程如下:

  1. 第一次载入 JavaScript 代码时,首先会创建一个全局环境。全局环境位于最外层,直到应用程序退出后(例如关闭浏览器和网页)才会被销毁。
  2. 每个函数都有自己的运行环境,当函数被调用时,则会进入该函数的运行环境。当该环境中的代码被全部执行完毕后,该环境会被销毁。不同的函数运行环境不一样,即使是同一个函数,在被多次调用时也会创建多个不同的函数环境。

每进入一个不同的运行环境时,JavaScript 都会创建一个新的执行上下文,该过程包括:

  1. 建立作用域链(Scope Chain);
  2. 创建变量对象(Variable Object,简称 VO);
  3. 确定 this 的指向。

创建变量对象

什么是变量对象呢?每个执行上下文都会有一个关联的变量对象,该对象上会保存这个上下文中定义的所有变量和函数。

而在浏览器中,全局环境的变量对象是 window 对象,因此所有的全局变量和函数都是作为 window 对象的属性和方法创建的。相应的,在 Node 中全局环境的变量对象则是 global 对象。

在 JavaScript 中,函数是一等公民,我们编写的代码更多是通过函数为基本单元来进行组织的。JavaScript 运行环境中出现次数更多的自然是函数环境,每进入一个新的函数环境,都会按照上述三个步骤创建一个新的执行上下文。

在函数环境下创建 VO 的时候,会创建我们熟知的 arguments 对象,然后检查函数声明和变量声明。

  • 对于变量声明:此时会给变量分配内存,并将其初始化为undefined(该过程只进行定义声明,执行阶段才执行赋值语句)。
  • 对于函数声明:此时会在内存里创建函数对象,并且直接初始化为该函数对象。

上述变量声明和函数声明的处理过程,便是我们常说的变量提升函数提升,其中函数声明提升会优先于变量声明提升。

因为变量提升容易带来变量在预期外被覆盖掉的问题,同时还可能导致本应该被销毁的变量没有被销毁等情况。因此 ES6 中引入了let 和 const 关键字,从而使 JavaScript 也拥有了块级作用域。

作用域

作用域分为静态作用域和动态作用域,在 JavaScript 中采用的是静态作用域(等同于词法作用域)。静态作用域的特点是,变量在编译阶段会生成一个确定的作用域。

作用域 = 词法环境 = 当前执行上下文

词法环境又分为两种:

  1. 变量环境:用来记录var/function等变量声明。
  2. 词法环境:用来记录let/const/class等变量声明。

作用域链

词法环境由两个成员组成:

  1. 环境记录(Environment Record):用于记录自身词法环境中的变量对象。
  2. 外部词法环境引用(Outer Lexical Environment):记录外层词法环境的引用。

通过外部词法环境的引用,作用域可以层层拓展,建立起从里到外延伸的一条作用域链。当某个变量无法在自身词法环境记录中找到时,可以根据外部词法环境引用向外层进行寻找,直到最外层的词法环境中外部词法环境引用为null,这便是作用域链的变量查询。

有如下代码:

function foo() {
  console.dir(bar);
  var a = 1;
  function bar() {
    a = 2;
  }
}
console.dir(foo);
foo();

打印结果如下:

Screen Shot 2022-01-20 at 7 29 13 PM

可以看到:

  • foo的[[scope]]属性包含了全局[[scope]]
  • bar的[[scope]]将会包含全局[[scope]]和foo的[[scope]]

JavaScript 会通过外部词法环境引用来创建变量对象的一个作用域链,从而保证对执行环境有权访问的变量和函数的有序访问。

编译阶段会进行变量对象(VO)的创建,该过程会进行函数声明和变量声明,这时候变量的值被初始化为 undefined。在代码进入执行阶段之后,JavaScript 会对变量进行赋值,此时变量对象会转为活动对象(Active Object,简称 AO),转换后的活动对象才可被访问,这就是 VO -> AO 的过程。

为了更好地理解这个过程,我们来看个例子,我们在foo函数中定义了变量b、函数c和函数表达式变量d:

function foo(a) {
  var b = 2;
  function c() {}
  var d = function() {};
}
foo(1);

在执行foo(1)时,首先进入定义期,此时:

  • 参数变量a的值为1
  • 变量b和d初始化为undefined
  • 函数c创建函数并初始化
AO = {
  arguments: {
    0: 1,
    length: 1
  },
  a: 1,
  b: undefined,
  c: reference to function c(){},
  d: undefined
}

进入执行期之后,会执行赋值语句进行赋值,此时变量b和d会被赋值为 2 和函数表达式:

AO = {
   arguments: {
    0: 1,
    length: 1
  },
  a: 1,
  b: 2,
  c: reference to function c(){},
  d: reference to FunctionExpression "d"
}

实际上在执行的时候,除了 VO 被激活,活动对象还会添加函数执行时传入的参数和arguments这个特殊对象,因此 AO 和 VO 的关系可以用以下关系来表达:

AO = VO + function parameters + arguments

虽然 JavaScript 代码的运行过程可以分为语法分析阶段、编译阶段和执行阶段,但由于在 JavaScript 引擎中是通过调用栈的方式来执行 JavaScript 代码的(下一讲会介绍),因此并不存在“整个 JavaScript 运行过程只会在某个阶段中”这一说法,比如上面例子中bar函数的编译阶段,其实是在foo函数的执行阶段中。

一般来说,当函数执行结束之后,执行期上下文将被销毁(作用域链和活动对象均被销毁)。但有时候我们想要保留其中一些变量对象,不想被销毁,此时就会使用到闭包。

确定 this 的指向

在 JavaScript 中,this指向执行当前代码对象的所有者,可简单理解为this指向最后调用当前代码的那个对象。

根据 JavaScript 中函数的调用方式不同,this的指向分为以下情况。

Screen Shot 2022-01-20 at 8 17 33 PM

实现 insertAfter

function insertAfter(newEl, targetEl) {
  const parentEl = targetEl.parentNode;

  if (parentEl.lastChild === targetEl) {
      parentEl.appendChild(newEl);
  } else {
      parentEl.insertBefore(newEl, targetEl.nextSibling);
  }
}

window.history.length

今天在修复一个 bug 时,看到 bug 描述是从课程列表页进入课程详情页后,来回切换左侧 tab 菜单,然后点击课程详情页返回按钮,预期是返回课程列表页,实际上返回的是上一个 tab 菜单页。

于是查了下资料:HTML 中的 History length 属性用于返回当前浏览器窗口历史列表中 URL 的计数。该属性返回的最小值是 1,因为当前页面是在此刻加载的,而可以显示的最大计数为 50。做了下测试,从打开一个浏览器窗口起,每打开一个页面,length 值就会加一,首次打开浏览器时会有默认页面,此时 length 为 1。

回到上述提到的 bug,我看下了源代码,发现里面所有的 tab 菜单页都监听了顶部导航栏里的返回按钮点击事件,有的处理逻辑是根据 history.length 是否大于 1 判定是否要回退到课程列表页的。

innerHTML 和 outerHTML 区别

  1. innerHTML 设置或获取位于对象起始和结束标签内的 HTML;
  2. outerHTML 设置或获取对象及其内容的HTML形式。
<div id="test"> 
   <span style="color:red">test1</span> test2 
</div>

innerHTML 是 <span style="color:red">test1</span> test2;outerHTML 是整段 HTML。

前端异常监控

在前端工程化方兴未艾的今天,很多人会更多关注项目上线前的工程化问题,比如如何生成模版项目(脚手架)、代码测试、编译构建、流水线 CI 和 CD 等,很少会关注项目上线后的一些异常问题。对于前端异常,很多时候开发者始终处于一个被动感知的状态,当用户反馈到开发者这边时,往往线上已经出现大量 case 了。后端一般会有告警服务,前端在这方面的能力就比较缺失,所以如何用工程化的手段去解决异常监控就很有必要了。

异常类型

按照 ECMA-262 里的规范来看,一共有如下类型的异常:

  • Error:基类异常
  • SyntaxError:语法异常
  • ReferenceError:引用异常
  • RangeError:范围异常
  • TypeError:类型异常
  • InternalError:内部异常
  • EvalError:Eval 方法异常
  • URIError:URI 相关方法产生的异常

自定义 Error

Error 是所有异常的基类,所有错误共享相同的属性:

  • Error.prototype.message
  • Error.prototype.name
  • Error.prototype.stack

当然,开发者也可以继承 Error 来自定义异常,一般而言一些库和框架会自定义异常。

异常监控

前端监控异常的方法如下:

  • window.onerror
  • 重写 setTimeout、setInterval 等方法,使用 try catch 来捕获错误
  • window.addEventListener('unhandledrejection', errorHandler) 捕获未处理的异步 reject
  • window.addEventListener('error', loadErrorHandler) 捕获资源加载异常
  • 重写 fetch、XMLHttpRequest 来捕获接口错误

同步异常还比较好处理,因为会有完整的错误堆栈等信息,而异步错误就存在语焉不详的问题,因为异步错误缺失执行前的上下文环境,所以对于异步错误还需要借助前后埋点等方式作为辅助去定位错误源头。

实现 Promise.all 方法

function promiseAll (arr = []) {
  return new Promise((resolve, reject) => {
    let ans = []
    let count = 0

    arr.forEach((item, index) => {
      Promise.resolve(item).then(res => {
        ans[index] = res // index 块级作用域确保结果数组顺序和原数组一致,也可以在 for 循环里使用 let
        count++ // 统计 resolve 个数

        if (count === arr.length) {
          resolve(ans)
        }
      }).catch(e => reject(e))
    })
  })
}

function task1 () {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(1)
    }, 2000)
  })
}

function task2 () {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(2)
    }, 100)
  })
}

let arr = [task1(), task2()]
promiseAll(arr).then(res => {
  console.log('ans ===', res) // 输出 [1, 2]
})

CSS 之 BFC

布局

页面布局分为流式布局、浮动布局和绝对定位布局三种,而 BFC 属于流式布局。

概念

Formatting context(格式化上下文) 是 W3C CSS2.1 规范中的一个概念。它是页面中的一块渲染区域,并且有一套渲染规则,它决定了其子元素将如何定位,以及和其他元素的关系和相互作用。

具有 BFC 特性的元素可以看作是隔离了的独立容器,容器里面的元素不会在布局上影响到外面的元素,并且 BFC 具有普通容器所没有的一些特性。

触发 BFC

  • body 根元素(自身属于流式布局)
  • 浮动元素:float 除 none 以外的值
  • 绝对定位元素:position (absolute、fixed)
  • display 为 inline-block、table-cells、flex
  • overflow 除了 visible 以外的值 (hidden、auto、scroll)

BFC 应用场景

  1. 解决外边距塌陷问题
  2. 解决浮动元素覆盖其它元素问题

ESM 和 CJS 模块规范区别

  • 两者的模块导入导出语法不同,CommonJs 是通过 module.exports,exports 导出,require 导入;ESModule 则是 export 导出,import 导入。
  • CommonJs 是运行时加载模块,ESModule 是在静态编译期间就确定模块的依赖。
  • ESModule 在编译期间会将所有 import 提升到顶部,CommonJs 不会提升 require。
  • CommonJs 导出的是一个值拷贝,会对加载结果进行缓存,一旦内部再修改这个值,则不会同步到外部。ESModule 是导出的一个引用,内部修改可以同步到外部。
  • CommonJs 中顶层的 this 指向这个模块本身,而 ESModule 中顶层 this 指向 undefined。
  • CommonJS 加载的是整个模块,将所有的接口全部加载进来,ESModule 可以单独加载其中的某个接口。

HTTP2 和 WebSocket 服务器推送区别

  • HTTP2 Server Push,一般用以服务器根据解析 index.html 同时推送 JPG/JS/CSS 等资源,而免了服务器发送多次请求。
  • WebSocket,需要服务器与客户端手动编写代码实现全双工通信,WebSocket 在 HTTP 协议之上升级后才可用。

如何区分基础组件和业务组件

得益于 MVVM 框架的流行,组件化方案成为了 webapp 开发中的标准,在业务开发中怎么区分基础组件和业务组件往往是需要一定技巧的。

基础组件

我个人的思考总结起来有这么几个特点:低耦合、原子性、高复用。低耦合是指基础组件不应该和其它组件耦合在一起;原子性是指基础组件作为一个基本单元只需要关注前端 UI 层面的输入和输出,其本身是高度自治的;高复用是指基础组件可以和其它基础组件或者是业务组件组合成更加复杂的组件。

业务组件

业务组件总结起来应该具有这么几个特点:高耦合、复杂性、低复用。高耦合是指我们在实现一个具体的业务需求时,往往需要组合基础组件成为一个新的组件;复杂性是指其除了需要关注前端 UI 层面的输入和输出,还需要关注和后端之间的交互;低复用是指业务组件一般是为了实现某个特定需求而开发出来的,当遇到不一样的需求时往往没办法实现复用。

css 自适应正方形实现

实现方式有 3 种:

  1. 父元素设置宽度,子元素撑开,然后设置 padding-bottom 等属性撑开,没有高度的情况下以宽度百分比计算;
  2. 通过 js 控制;
  3. 使用 aspect-ratio 属性:aspect-ratio: 1 / 1;

服务器推送技术 SSE

SSE 是 Server-Sent Events 的简称, 是一种服务器端到客户端(浏览器)的单项消息推送。对应的浏览器端实现 Event Source 接口被制定为HTML5 的一部分。不过现在IE不支持该技术。相比于 WebSocket,SSE 简单很多,服务器端和客户端工作量都要小很多、简单很多,同时实现的功能也要有局限。

原理

如果 http 版本处于 2.0 以下,那么 http 是没办法做到服务器主动推送消息到客户端的。但是,有一种变通方法,就是服务器向客户端声明,接下来要发送的是流信息(streaming)。

也就是说,发送的不是一次性的数据包,而是一个数据流,会连续不断地发送过来。这时,客户端不会关闭连接,会一直等着服务器发过来的新的数据流,视频播放就是这样的例子。本质上,这种通信就是以流信息的方式,完成一次用时很长的下载。

SSE 就是利用这种机制,使用流信息向浏览器推送信息。它基于 HTTP 协议,目前除了 IE/Edge,其他浏览器都支持。

特点

和 WebSocket 相比,SSE 技术有如下特点:

  • SSE 使用 HTTP 协议,现有的服务器软件都支持。WebSocket 是一个独立的新协议。
  • SSE 属于轻量级,使用简单;WebSocket 协议相对复杂。
  • SSE 默认支持断线重连,WebSocket 需要自己实现。
  • SSE 一般只用来传送文本,二进制数据需要编码后传送,WebSocket 默认支持传送二进制数据。
  • SSE 支持自定义发送的消息类型。

关于 SSE 技术的具体细节,可以阅读阮一峰的博客Server-Sent Events 教程

对象数组去重

有一个未知的对象数组,数组的每一项存储的都是对象,现在需要对这个数组去重。这个和我们常见的对象数组不太一样,以往我们会根据对象数组里对象的某些属性去重,而这道题我们无法得知对象里的属性是什么,所以需要采取最直观的方式去重。

function dropDuplicate (arr = []) {
  if (arr.length <= 1) return arr

  const isSameObj = (a = {}, b = {}) => {
    if (typeof a !== 'object' || typeof b !== 'object') return false

    if (Object.keys(a).length !== Object.keys(b).length) return false

    for (const [key, value] of Object.entries(a)) {
      if (typeof value === 'object' || typeof b[key] === 'object') {
        return isSameObj(value, b[key])
      }
      if (!b[key] || b[key] !== value) return false
    }

    return true
  }

  let dropIdx = []
  for (var i = 0, len = arr.length; i < len - 1; i++) {
    for (var j = i + 1; j < len; j++) {
      if (isSameObj(arr[i], arr[j])) dropIdx.push(j)
    }
  }

  dropIdx.forEach(index => arr.splice(index, 1, null))

  return arr.filter(item => item !== null)
}

let objArr = [
  {
    name: 'chuck',
    gender: 'male',
    children: {
      name: 'nick',
      bro: []
    }
  },
  {
    name: 'chuck',
    gender: 'male',
    children: {
      name: 'nick',
      bro: []
    }
  },
  {
    name: 'henry',
    gender: 'male'
  },
  {
    name: 'mary',
    gender: 'female'
  },
  {
    name: 'mary',
    gender: 'female'
  }
]

console.log(dropDuplicate(objArr))
// 输出如下
// [
//   {
//     name: 'chuck',
//     gender: 'male',
//     children: { name: 'nick', bro: [] }
//   },
//   { name: 'henry', gender: 'male' },
//   { name: 'mary', gender: 'female' }
// ]

我只想到了这种时间复杂度为 O(n * n) 的解法,如果看到的同学有 O(n) 的方案,欢迎补充。

代理线上资源调试代码

提到代理我们会想到 nginx 和 Charles,nginx 可以直接将特定目录下的资源代理到指定的目录,其既可以是服务器上的资源,也可以是本地启动 docker 将文件系统挂起后的本地资源;Charles 更多时候是用来抓包各种请求,它也可以像 nginx 那样批量代理,比如针对某个域名路径后接通配符来达到批量代理的目标。

1、nginx

location  /statics/  {
 alias /project/statics/;
}

2、Charles

抓取到需要代理的请求后右击可以选择 map local,有时候如果想直接改线上代码,这种方式很合适。https://blog.csdn.net/Moonlight_16/article/details/118726585

实现 Promise.retry 方法

// 请实现一个方法, 对传入的异步任务尝试n次
// 若异步任务小于等于n次执行的某次为fullfilled, 则返回这次成功的结果
// 若异步任务n次执行均为rejected, 则返回最后的一次错误

async function tryNTimes (asyncFn, n = 5) {
  let err
  let count = 1

  while (n--) {
    try {
      console.log(`第${count++}次尝试`)
      const ret = await asyncFn()
      return Promise.resolve(ret)
    } catch (e) {
      err = e
      continue
    }
  }

  return Promise.reject(err)
}

function task () {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // 生成一个[0, 10] 的整数
      let random = Math.floor(Math.random() * 11)
      console.log('生成的随机数:', random)
      if (random < 2) {
        console.log('任务成功')
        resolve(random)
      } else {
        reject(new Error('任务失败'))
      }
    }, 1000)
  })
}

tryNTimes(task)

运行结果如下:

Screen Shot 2022-09-25 at 16 06 10

koa 洋葱模型原理

function compose (middleware = []) {
  return function (context, next) {
    function dispatch (index) {
      let fn = middleware[index]
      if (index === middleware.length) fn = next

      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, function () {
          return dispatch(++index)
        }))
      } catch (err) {
        return Promise.reject(err)
      }
    }

    return dispatch(0)
  }
}

setTimeout 链式调用

有这样一段代码:

let t1 = setTimeout(() => {
  console.log(1)
  let t2 = setTimeout(() => {
    console.log(2)
    let t3 = setTimeout(() => {
      console.log(3)
    }, 3000)
  }, 2000)
}, 1000)

这段代码将会在 1s、3s 和 6s 时分别打印 1、2、3。当定时器过多时,这种嵌套会导致代码臃肿。为了解决这个问题,思路就是如何实现定时器链式调用。

提起链式调用就会想到 ES6 中的 Promise.then,所以问题就是怎么把每个定时器转换为 Promise。默认情况下,每一个 .then() 方法还会返回一个新生成的 promise 对象,这个对象可被用作链式调用。

将一个定时器转换为 Promise 其实就是常见的 sleep 方法:

let sleep = function (time = 0) {
  return new Promise(resolve => setTimeout(resolve, time))
}

接下来要做的事情就是自定义 .then 方法的返回值,为了实现定时器链式调用需要直接返回 sleep。

let t = sleep(1000).then(() => {
  console.log(1)
  return sleep(2000)
}).then(() => {
  console.log(2)
  return sleep(3000)
}).then(() => {
  console.log(3)
})

前端模块化

CommonJS

Node 应用由模块组成,采用 CommonJS 模块规范。也就是说CommonJs是应用在node服务器端的,如果浏览器想使用CommonJs规范的话需要用 browserify 库来进行转化。

1、module

Node内部提供一个Module构建函数,所有模块都是Module的实例。

function Module(id, parent) {
  this.id = id;
  this.exports = {};
  this.parent = parent;
  // ...
}

每个模块内部,都有一个module对象,代表当前模块。它有以下属性。

  1. module.id 模块的识别符,通常是带有绝对路径的模块文件名。
  2. module.filename 模块的文件名,带有绝对路径。
  3. module.loaded 返回一个布尔值,表示模块是否已经完成加载。
  4. module.parent 返回一个对象,表示调用该模块的模块。
  5. module.children 返回一个数组,表示该模块要用到的其他模块。
  6. module.exports 表示模块对外输出的值。

2、require

module.exports属性表示当前模块对外输出的接口,其它文件引入模块则使用 require,其有如下两个特性:

  1. 第一次加载某个模块时,Node会缓存该模块。
  2. 引入的是被输出的值的拷贝。
  3. 同步加载,如果加载过长,会阻塞后续代码执行。

AMD

异步加载模块的方式。通过 define 传递一个函数来定义模块,引用模块则使用 require 函数,第一个参数是模块名,第二个参数则是回调函数,回调参数就是引入的模块名。

UMD

通过对 CommonJs、CMD、AMD 进一步处理,它没有自己专有的规范,是集结了 CommonJs、CMD、AMD 的规范于一身。

ESModule

export 导出,import 导入,可以实现按需加载、条件加载。与 commonjs 的区别如下:

  1. CommonJS 模块输出的是一个值的拷贝,ESM 模块输出的是值的引用;
  2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口,因此可以做静态代码分析、tree shaking 等;
  3. CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。

Ajax 和 Fetch 的区别

  1. Ajax 是理用 XMLHttpRequest 对象来请求数据的,而 Fetch是 window 的一个方法;
  2. Ajax 基于原生的 XHR 开发,XHR 本身的架构不清晰,已经有了 fetch 的替代方案;
  3. Fetch 比 Ajax 有着更好更方便的写法,它的 API 是基于 Promise 实现的,但不使用回调函数,对低版本浏览器不兼容;
  4. Fetch 只对网络请求报错,对 404、500 都当做成功的请求,需要封装去处理;
  5. Fetch 没有办法原生监测请求的进度,而 XHR 可以;
  6. fetch默认不会带cookie,需要添加配置项: fetch(url, {credentials: 'include'});
  7. fetch不支持abort,不支持超时控制,使用setTimeout及Promise.reject的实现的超时控制并不能阻止请求过程继续在后台运行,造成了流量的浪费。

video 底部空白问题

和 img 元素表现类似,如果在 video 上添加背景色,会看到底部有多出来的空余部分。video 属于行内元素(demo),img 也可以看做是行内元素,所以出现留白问题是 line-height 和 vertical-align 造成的,解决办法如下:

  1. 自身设置 vertical-align: bottom;
  2. 父元素设置 font-size: 0,当字体大小为 0 时也就不存在行高问题了;
  3. 自身设置 display: block | flex 等块级布局。

Chrome 中的 memory cache 和 disk cache

我目前的开发方式是通过 docker 启动一个 container,然后在把本机的工作目录挂载到 container 里,这样的话很方便,具体可以阅读 https://docs.docker.com/get-started/06_bind_mounts/。在一次开发调试时,发现 Chrome 预览没有生效,经过查看资源请求信息,发现很多资源不是 from memory cache 就是 from disk cache,而这就导致更改一直未生效。我于是把 Chrome 控制面板中的 Disable cache 勾选了,刷新页面加载到了最新的本地资源。本着好奇心,我接下来花了点时间探索了下 Chrome 的缓存策略。

memory cache 是从浏览器的内存空间(RAM)中访问缓存信息,所以读写速度更快,但它的生命周期更短;而 disk cache 是从磁盘访问的,读写速度较慢,它属于持久缓存。发现一个有意思的地方,就是 css 资源是 from disk cache,而 js、图片、字体等资源则是 from memory cache。关于 Chrome 的缓存策略可以阅读 https://www.chromium.org/developers/design-documents/network-stack/disk-cache/。

首屏、白屏时间计算

白屏

浏览器开始渲染 标签或者解析完 标签的时刻就是页面白屏结束的时间点。也就是说从显示内容前的时间。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>白屏</title>
  <script type="text/javascript">
    // 不兼容performance.timing 的浏览器,如IE8
    window.pageStartTime = Date.now();
  </script>
  <!-- 页面 CSS 资源 -->
  <link rel="stylesheet" href="common.css">
  <link rel="stylesheet" href="page.css">
  <script type="text/javascript">
    // 白屏时间�结束点
    window.firstPaint = Date.now();
  </script>
</head>
<body>
  <!-- 页面内容 -->
</body>
</html>

可使用 Performance API 时

白屏时间 = firstPaint - performance.timing.navigationStart;

不可使用 Performance API 时

白屏时间 = firstPaint - pageStartTime;

首屏

首屏时间是指用户打开网站开始,到浏览器首屏内容渲染完成的时间。对于用户体验来说,首屏时间是用户对一个网站的重要体验因素。

首屏时间计算方法有:

  1. 首屏模块标签标记法(不常用)
  2. 统计首屏内加载最慢的图片的时间
  3. 自定义首屏内容计算法

由于统计首屏内图片完成加载的时间比较复杂。因此我们在业务中通常会通过自定义模块内容,来简化计算首屏时间。如下面的做法:

  • 忽略图片等资源加载情况,只考虑页面主要 DOM
  • 只考虑首屏的主要模块,而不是严格意义首屏线以上的所有内容

注意:首屏时间是包括了白屏时间的,因此可以使用 performance.timing.navigationStart 作为开始时间。

实现一个插值表达式处理函数

// 请实现一个 render 函数:该函数接受两个参数,一个是模版,一个是数据对象。
// 模版通过插值表达式引用数据对象上的数据,通过 render 函数能够把模版转换为真实的字符串。

let template = '大家好,我的名字叫做{{ name }},我是一名{{ job }}。'

let data = {
  name: 'Henry',
  gender: 'male',
  job: 'software engineer'
}

function render (template = '', data) {
  if (data === null) return

  const MUSTACHE_REG = /\{\{.*?\}\}/ig
  const KEY_REG = /(?<=\{\{(\s*?)).*?(?=(\s*?)\}\})/ig
  
  return template.replace(MUSTACHE_REG, function (match = '') {
    let key = match.match(KEY_REG)[0].trim()
    return data[key]
  })
}

console.log(render(template, data)) // 大家好,我的名字叫做Henry,我是一名software engineer。

实现一个比较详细的深拷贝

function deepClone (origin = {}, target = {}, map = new WeakMap()) {
  if (typeof origin !== 'object') return origin
  if (origin === null) return null
  if (origin instanceof RegExp) return new RegExp(origin)
  if (origin instanceof Date) return new Date(origin)
  if (origin instanceof Set) {
    let _set = new Set()
    origin.forEach(item => _set.add(item))
    return _set
  }
  if (origin instanceof Map) {
    let _map = new Map()
    origin.forEach((val, key) => _map.set(key, val))
    return _map
  }

  if (map.has(origin)) return map.get(origin)
  map.set(origin, target)

  for (const key in origin) {
    if (origin.hasOwnProperty(key)) {
      if (typeof origin[key] !== 'object') {
        target[key] = origin[key]
      } else {
        target[key] = Array.isArray(origin[key]) ? [] : {}
        target[key] = deepClone(origin[key], target[key], map)
      }
    }
  }

  return target
}

let set = new Set()
set.add(4).add(5).add(6)

let map = new Map()
map.set('name', 'chuck').set('gender', 'male')

let obj = {
  a: 1,
  b: {
    c: '2',
    d: [1, 2, 3]
  },
  reg: new RegExp('reg'),
  date: new Date('2022/9/27'),
  set,
  map,
  getName: () => {
    return 'chuck'
  },
  undef: undefined,
  empty: null,
  symbol: Symbol('symbol')
}

let obj2 = {
  a: 2,
  next: obj
}

obj.next = obj2
let obj3 = Object.create(obj)
obj3.a = 1

console.log('deepClone', deepClone(obj))

Screen Shot 2022-09-27 at 20 22 59

立即执行防抖函数

function debounce(func, wait) {
  let timer = null
  let flag = true
  return function () {
    clearTimeout(timer)
    if (flag) {
      func.apply(this, arguments)
      flag = false
    }
    timer = setTimeout(() => { flag = true }, wait)
  }
}

在 vue 里使用:

methods: {
  toPurchaseNow: debounce(async function () {
    // do something
  }, 1000)
}

Git 使用

Git 恢复修改文件

Git 提交的工作流程是:工作区 ——> 暂存区 ——> 仓库区,如果要恢复已经提交的文件,那么就需要反过来执行这一个流程。

所以按照分类,提交的文件存在 3 种情况:

  1. 只是进行了修改操作,仍处于工作区;
  2. 修改了文件并已经提交,处于暂存区;
  3. 修改了文件并已经提交,处于仓库区。

那么对应的就有 3 种解决方法,首先来看如何处理第一种情况。

  1. 只需要执行一条命令 git checkout -- README.md;
  2. 由于还没有生成提交记录 hash 值,HEAD 指向还没有更改,所以可以通过 git reset HEAD 命令回退到当前版本,然后再撤销文件修改;
  3. 由于提交到仓库生成了 hash 提交记录值,所以需要通过 git reset HEAD^ 命令回退到上一个版本,然后再恢复修改文件。

Git 分支重命名

1、对于本地分支

git branch -m oldName newName

2、对于远程分支

删除远程分支:

git push --delete origin oldName

再推送本地已更改分支到远程:

git push -u origin newName

Git 拉取源代码

当项目过大时,git clone 会出现超时失败,这时候我们可以只拉去最新的一次或者几次 commit:

git clone projectName --depth=1

如果需要获取历史提交代码,就需要重新拉取:

git pull --unshallow

CDN 缓存机制和回源策略

CDN 工作概述

客户端先检查本地是否有缓存以及缓存是否过期,如果过期,则像 CDN 边缘节点发起请求。CDN 边缘节点会检查用户请求数据的缓存是否过期,如果没有过期,直接响应用户的请求,完成一次 http 请求和响应。如果 CDN 缓存数据过期,那么还需要向源站发送回源请求(back to the source request),来拉取最新的数据。

CDN 的典型拓扑图如下:

CDN 层级划分

  • 边缘层:CDN 中直接面向用户,负责给用户提供内容服务的 cache 设备直接部署在整个 CDN 网络的边缘位置。
  • 中心层:负责全局的管理和控制,同时也拥有最多的 cache。在边缘层设备没有命中 cache 时,会向中心层设备请求;而中心层未能命中则会向源站请求。
  • 区域层:如果 CDN 系统非常庞大,边缘层向中心层请求太多,会造成中心层负载压力过大。此时会在边缘层和中心层之间添加一层区域层,负责一个区域的管理和控制。

CDN 缓存策略

CDN 边缘节点缓存策略因服务商不同而不同,但一般都会遵循 http 标准协议,通过 http 响应头中的 Cache-control: max-age 的字段来设置 CDN 边缘节点数据缓存时间。

当客户端向 CDN 节点请求数据时,CDN 节点会判断缓存数据是否过期,若缓存数据并没有过期,则直接将缓存数据返回给客户端;否则,CDN 节点就会向源站发出回源请求,从源站拉取最新数据,更新本地缓存,并将最新数据返回给客户端。

CDN 服务商一般会提供基于文件后缀、目录多个维度来指定 CDN 缓存时间,为用户提供更精细化的缓存管理。

CDN 缓存时间会对“回源率”产生直接的影响。若 CDN 缓存时间较短,CDN 边缘节点上的数据会经常失效,导致频繁回源,增加了源站的负载,同时也增大的访问延时;若 CDN 缓存时间太长,会带来数据更新时间慢的问题。开发者需要增对特定的业务,来做特定的数据缓存时间管理。

CDN 缓存刷新

CDN 边缘节点对开发者是透明的,相比于浏览器的强制刷新来使浏览器本地缓存失效,开发者可以通过 CDN 服务商提供的“刷新缓存”接口来达到清理 CDN 边缘节点缓存的目的。这样开发者在更新数据后,可以使用“刷新缓存”功能来强制 CDN 节点上的数据缓存过期,保证客户端在访问时,拉取到最新的数据。

CDN 缓存缺点

CDN 的分流不仅减少了用户的访问延时,也减少了源站的负载压力。但是缺点很明显,当源站数据更新时,如果 CDN 节点过多没有及时更新,那么用户访问到的还是过期的资源,涉及到 js 的脚本资源很可能会导致用户访问异常。

CDN 优点

  • 提升网站加载性能,这个是作为前端开发第一时间会想到的。
  • 实现跨运营商、跨地域的全网覆盖。比如国际化站点不同的语言包存放于不同的 CDN 节点,而不同的 CDN 节点服务运营商不同。
  • 保障网站安全。正所谓不能把鸡蛋放入一个篮子里,网站的不同资源部署在不同的 CDN 服务器上,无形中增加了网络攻击的成本。
  • 便于容灾。当某个 CDN 节点服务发生故障时,将会由其它 CDN 节点继续提供服务,而且在修复故障机器时,可以从源站获取到备份资源。
  • 让研发专注于业务本身。

当 CDN 挂了怎么办

CDN 如果挂了,应用就会受到影响,所以可以通过启用本地资源的方式来作为兜底策略。代码实现如下:

<script src="http://cdn.static.runoob.com/libs/jquery/1.10.2/jquery.min.js"></script>
<script>window.jQuery || document.write('<script src="js/vendor/jquery-1.10.2.min.js"><\/script>')</script>

一般而言一个网站的资源存放于不同的域名,可以把本地资源路径替换为主域地址。可以阅读当CDN主域挂了该如何应对(前端网站容灾方案)

为什么一个网站的资源会存在于多个域名中?

  • 更方便 CDN 缓存
  • 突破浏览器并发限制
  • 节省 cookie 带宽(涉及用户登录时才会用到 cookie)
  • 节省主域名连接数

域名发散作为网络性能优化手段之一,并不是说越多域名越好,因为 DNS 解析域名也会有开销。而且因为很多网站采用的是 https 协议,需要安装更多的证书,不利于快速部署。

实现洋葱模型

// 写一个 Task 类
 
class Task {
  constructor() {
    // coding here
    this.queue = []
    this.flag = false
  }

  add(fn, context, ...args) {
    // coding here
    let task = {
      fn,
      context,
      args: [...args]
    }
    this.queue.push(task)
    return this
  }

  run() {
    // coding here
    let len = this.queue.length

    const func = (index = 0) => {
      if (this.flag) return Promise.reject(new Error('Follow-up tasks are stopped.'))
      if (index === len) return Promise.resolve()

      const task = this.queue[index]
      return Promise.resolve(task.fn.apply(task.context, [() => func(++index), ...task.args]))
    }

    return func()
  }

  stop() {
    // coding here
    // this.queue = []
    this.flag = true
  }
}


// 满足

function task1(next) {
  setTimeout(() => {
    console.log(1)
    next()
  }, 1000)
}

function task2(next, a) {
  // this.stop()
  console.log(this.queue)
  setTimeout(() => {
    console.log(a)
    next()
  }, 1000)
}

function task3(next, b, c) {
  setTimeout(() => {
    console.log(b)
    console.log(c)
    next()
  }, 1000)
}

const task = new Task()
task.add(task1).add(task2, task, 2).add(task3, task, 3, 4)
task.run()


// 备注:当任务函数执行 next 的时候,会跳转到下一个任务函数执行

实现带定时器的异步队列

class Queue {
  constructor () {
    this.tasks = []
  }

  task (time, fn) {
    this.tasks.push({ fn, time })
    return this
  }

  async start () {
    for (const item of this.tasks) {
      const { fn, time } = item
      await this.timeout(time).then(fn)
    }
  }

  timeout (time) {
    return new Promise(resolve => setTimeout(resolve, time))
  }
}

new Queue()
  .task(1000, () => console.log(111))
  .task(2000, () => console.log(333))
  .task(1000, () => console.log(444))
  .start()

实现 lodash 中 get 方法

lodash 中的这个方法是用于获取一个变量里的值的,因为很多时候通过点式访问很可能报错,所以希望对路径不存在的返回默认值。

function _get(obj, keys, defaultVal) {
    let lists
    if (Array.isArray(keys)) {
        lists = keys
    } else {
        lists = keys.replace(/\[/g, '.').replace(/\]/g, '').split('.') // 字符串直接根据 . 分割为数组
    }

    let ret = lists.reduce((acc, curKey) => {
        return (acc || {})[curKey] // 重点是利用好这个 acc 累积器,当属性不存在的时候返回的是 undefined
    }, obj)
    return ret || defaultVal
}

var obj = {
  a: [
    {
      b: {
        c: 3,
      },
    },
  ],
  e: {
    f: 1,
  },
};

console.log(_get(obj, 'e.f')); // 1
console.log(_get(obj, ['e','f'])) // 1
console.log(_get(obj, 'a.x')); // undefined
console.log(_get(obj, 'a.x', '--')) // -- 
console.log(_get(obj, 'a[0].b.c')) // 3 => 'a.0.b.c' => ['a', '0', 'b', 'c']
console.log(_get(obj, ['a', 0, 'b', ,'c'])) // 3

浏览器进程和线程

如何理解浏览器的进程和线程呢?其实在浏览器中每打开一个 tab 页面,就开启了一个进程。由于在一个进程下,可以有多个线程,所以浏览器进程下又划分了 GUI 渲染线程和 JavaScript 引擎线程。由于 JavaScript 引擎线程在执行中是可能会更改 DOM 的,所以出于线程安全考虑,GUI 渲染线程和 JavaScript 引擎线程是互斥的,同一时间下只会有一个掌握有控制权(执行权)。

前端网络安全

XSS 跨站脚本攻击

原理

XSS 是常见的 Web 攻击技术之一.所谓的跨站脚本攻击指得是:恶意攻击者往 Web 页面里注入恶意 Script 代码,用户浏览这些网页时,就会执行其中的恶意代码,可对用户进行盗取 cookie 信息、会话劫持等各种攻击。

危害

1.盗取各类用户帐号,如机器登录帐号、用户网银帐号、各类管理员帐号
2.控制企业数据,包括读取、篡改、添加、删除企业敏感数据的能力
3. 盗窃企业重要的具有商业价值的资料
4.非法转账
5.强制发送电子邮件
6.网站挂马
7.控制受害者机器向其它网站发起攻击

前端如何处理

  • 过滤用户的输入信息,禁止用户在输入的过程中输入 "<", ">", "引号", "$", "_"
  • 核心的用户身份标示或 token 保存在 Cookie 中,Cookie 中一定要加 “HTTPOnly” 在结尾,保证只有在 html 操作时才能将 cookie 中的内容发送出去,在 JS 中无法获得用户的 Cookie 信息
  • 启用 CSP(Content Security Policy) 策略。在服务端使用 HTTP的 Content-Security-Policy 头部来指定策略,或者在前端设置 meta 标签。
Content-Security-Policy: default-src 'self'
<meta http-equiv="Content-Security-Policy" content="form-action 'self';">

CSRF 跨站请求伪造

原理

CSRF(Cross-site request forgery)跨站请求伪造,也被称为 “One Click Attack” 或者 Session Riding,通常缩写为 CSRF 或者 XSRF,是一种对网站的恶意利用。尽管听起来像跨站脚本(XSS),但它与 XSS 非常不同,XSS 利用站点内的信任用户,而 CSRF 则通过伪装来自受信任用户的请求来利用受信任的网站。与 XSS 攻击相比,CSRF 攻击往往不大流行(因此对其进行防范的资源也相当稀少)和难以防范,所以被认为比 XSS 更具危险性。

前端如何处理

  • 减少在 cookie 中存储客户核心内容比如用户的 token、ID、access_token 等
  • GET 请求不对数据进行修改
  • 不让第三方网站访问到 Cookie,设置 Samesite Cookie 属性
  • 阻止第三方网站请求接口
  • 添加验证码(体验不好)
  • 判断请求的来源:检测Referer(并不安全,Referer可以被更改)
  • 使用Token(主流)

点击劫持

原理

点击劫持是指在一个Web页面中隐藏了一个透明的iframe,用外层假页面诱导用户点击,实际上是在隐藏的frame上触发了点击事件进行一些用户不知情的操作。

前端如何处理

  • frame busting
if ( top.location != window.location ){
    top.location = window.location
}
  • X-Frame-Options

X-FRAME-OPTIONS是微软提出的一个http头,专门用来防御利用iframe嵌套的点击劫持攻击。并且在IE8、Firefox3.6、Chrome4以上的版本均能很好的支持。

DDOS 攻击

原理

DDOS 攻击,它在短时间内发起大量请求,耗尽服务器的资源,无法响应正常的访问,造成网站实质下线。

前端如何处理

  • 防范 DDOS 的第一步,就是你要有一个备份网站,或者最低限度有一个临时主页。生产服务器万一下线了,可以立刻切换到备份网站,不至于毫无办法。

后端如何处理

  • HTTP 请求的拦截,恶意请求都是从某个 IP 段发出的,那么把这个 IP 段封掉就行了。或者,它们的 User Agent 字段有特征(包含某个特定的词语),那就把带有这个词语的请求拦截。
  • 带宽扩容,或者使用 CDN

参考资料

浏览器为什么要请求并发数限制?

  1. 减少操作系统端口资源消耗

PC总端口数为65536,那么一个TCP(http也是tcp)链接就占用一个端口。操作系统通常会对总端口一半开放对外请求,以防端口数量不被迅速消耗殆尽。

  1. 过多并发导致频繁切换产生性能问题

一个线程对应处理一个http请求,那么如果并发数量巨大的话会导致线程频繁切换。而线程的上下文切换有时候并不是轻量级的资源。这导致得不偿失,所以请求控制器里面会产生一个链接池,以复用之前的链接。所以我们可以看作同域名下链接池最大为4~8个,如果链接池全部被使用会阻塞后面请求任务,等待有空闲链接时执行后续任务。

  1. 避免同一客服端并发大量请求超过服务端的并发阈值

在服务端通常都对同一个客户端来源设置并发阀值避免恶意攻击,如果浏览器不对同一域名做并发限制可能会导致超过服务端的并发阀值被 BAN 掉。

  1. 客户端良知机制

为了防止两个应用抢占资源时候导致强势一方无限制的获取资源导致弱势一方永远阻塞状态。

微前端 js 和 css 怎么隔离

样式隔离主要有以下几种方案:

  1. BEM(Block Element Modifier) 约定项目前缀;
  2. CSS-Modules 打包时生成不同的选择器名(Vue scope 采用这种方式);
  3. Shadow Dom 真正意义上的隔离;
  4. css-in-js

js 隔离方案:

  1. iframe
  2. proxy

缓存函数

面试中面试官让手写一个缓存函数,然后自己其实没有怎么看过这方面知识,按照自己的认知利用闭包和 map 写了出来。其实我之前在看 vue 源码的时候有注意到计算属性是基于缓存函数实现的,但时间有点久了,后面搜了下有比较简洁的写法:

function memorize (func, context) {
  let cache = Object.create(null)
  context = context || this
  return (...key) => {
    if (!cache[key]) {
      cache[key] = func.apply(context, key)
    }
    return cache[key]
  }
}

实现单例 EventEmitter

class EventEmitter {
  constructor () {
    if (!EventEmitter.instance) {
      this.task = {}
      EventEmitter.instance = this
    }
    return EventEmitter.instance
  }

  on (type, handler) {
    if (!this.task[type]) this.task[type] = []
    this.task[type].push(handler)
  }

  emit (type) {
    if (!this.task[type]) return

    const args = Array.from(arguments).slice(1)
    const handlers = this.task[type]
    for (var i = 0, len = handlers.length; i < len; i++) {
      const handler = handlers[i]
      handler(...args)
    }
  }
}

// var EventEmitter = (function () {
//   var instance
//   return function () {
//     if (!instance) {
//       instance = new BaseEventEmitter()
//       return instance
//     }
//     return instance
//   }
// })()

const eventBus = new EventEmitter()
const eventBus1 = new EventEmitter()

console.log(eventBus === eventBus1) // 打印输出: true

function handleClick(param1, param2) {
  console.log(param1, param2)
}

eventBus.on('click', handleClick)
eventBus.emit('click', 'foo', 'bar') // 打印输出: foo bar

JavaScript 中的任务

我们经常会看到同步任务和异步任务的文章,而且对这些概念的认知都根深蒂固了,但什么是任务呢?下面是一段对任务的描述:

A task is any JavaScript code which is scheduled to be run by the standard mechanisms such as initially starting to run a program, an event callback being run, or an interval or timeout being fired. ... An event fires, adding the event's callback function to the task queue.

任务是由标准机制安排运行的任何 JavaScript 代码,如最初开始运行程序,事件回调被运行,或间隔或超时被触发。一个事件被触发,将事件的回调函数添加到任务队列中。

求两个数组的交集

有这样两个 number 类型的数组,需要求两个数组之间的交集。如果不借助 API,最先想到的是通过 for 循环来解决。

function findCommon (arr1, arr2) {
  var arr = []
  for (var i = 0; i < arr1.length; i++) {
    for (var j = 0; j < arr2.length; j++) {
      if (arr1[i] === arr2[j]) arr.push(arr1[i]) 
    }
  }
  return arr
}

这样虽然能满足要求,但是太原始了,时间复杂度始终是 O(m*n)。所以优化的点就是把时间复杂度 O(m*n) 给 降低到 O(m+n)。

因此想到了滑动比较,滑动比较需要对数组排序,这样才好利用排序的特征。滑动比较的具体操作是,采用双下标或者双指针,从头开始比较。如果一个值比另一个小,那么只需要移动小的那个的下标或者指针;如果两个值相等,那么这个值在交集里,保存该值,然后下表或者指针同时往前移动;重复前两步操作,直到下标或者指针超过数组长度为止。

有了这样的思路,接下来就是具体的编码实现了。第一步需要对数组进行排序,这里对两个数组进行升序排序。

function findCommon (arr1 = [], arr2 = []) {
  arr1 = arr1.sort((a, b) => a - b)
  arr2 = arr2.sort((a, b) => a - b)

  var i = 0,
    j = 0,
    arr = []

  while (i < arr1.length && j < arr2.length) {
    if (arr1[i] < arr2[j]) {
      i++
    } else if (arr1[i] === arr2[j]) {
      arr.push(arr1[i])
      i++
      j++
    } else {
      j++
    }
  }

  return arr
}

由于多了一步排序操作,所以整体的复杂度,还需要加上 sort 方法的时间复杂度。对于 sort 这个 API,不同浏览器厂商的实现方式不一样,具体可以查看深入浅出 JavaScript 的 Array.prototype.sort 排序算法

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.