Git Product home page Git Product logo

lizz-blog's Introduction

Hi there 👋

I'm lizhongzhen!

I'm still very weak and my code is not good enough, but i will keep moving.

Anyway, thankyou for visiting!

lizz-blog's People

Contributors

lizhongzhen11 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  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

lizz-blog's Issues

keep-alive组件之include属性业务实践——动态改变组件keep-alive状态

需求

昨天接了个项目,维护别人开发好的后台管理系统。
业务场景:点击左侧菜单栏,右侧渲染对应路由页面并在头部有对应的tags标签,点击tag标签也相当于路由跳转到对应页面。

要求:

  1. 点击菜单栏时,右侧面板顶部需要有与点击过的菜单一一对应的tab/tag标签,点击该标签也是跳转到对应路由,标签可关闭
  2. 点击菜单栏时,将对应页面重新渲染,清空数据
  3. 点击tag标签时,如果已经填写数据,那么tag来回切换时保留数据不清空

分析

这种需求其实比较常见,主要是为了操作方便。
但是如何去做?怎么去思考呢?

假设有a,b,c三个菜单,原先功能是点击a,b,c三个菜单,右侧面板会相应的渲染这三个菜单对应的组件/页面

思考要求1

针对要求1,较为简单,vue相关的ui库大多提供了tab/tag组件,这里我选用tag,毕竟用tab相当于把所有页面组件全放到tabs里面渲染,这样并不好。

这个项目比较坑的就是菜单栏是写死的,不是通过config.js配置的,这里浪费了很多时间,我需要把所有路由以及对应的菜单名称都维护到一个数组route里面,然后当我点击菜单时,根据菜单路由去route中找到对应的信息,然后放到tags数组里面,渲染tag标签。
同时,给tag加个click事件,点击进行路由跳转。

思考要求2

其实目前就是路由跳转重新渲染,暂时还不需要去考虑。

思考要求3

点击tag标签进行路由跳转,但是不能清空数据。
熟悉的vue的人肯定立马想到keep-alive组件。

<keep-alive>
  <router-view></router-view>
</keep-alive>

加上上面代码,试验下,信息确实保留了。

但是,问题也来了!
此时点击菜单栏,无法去重新刷新页面并清空数据了!

如何让点击菜单重新渲染页面和点击tag标签保留页面数据共存呢?

一开始,我想了一种方法:

<keep-alive v-if="isKeepAlive">
  <router-view></router-view>
</keep-alive>
<router-view v-if="!isKeepAlive"></router-view>

通过两个不同的router-view去渲染组件,点击tag标签跳转的走keep-alive,点击菜单跳转不走keep-alive

经过试验,依然不行。当我点击a,b,c三个菜单,右侧有对应的三个tag标签后,我通过点击tag标签切换页面并分别在这三个页面上写点数据,然后点击a菜单去重新渲染并清空a页面,然后我点击tag标签切换到b页面,再点击tag标签切回a页面,发现之前数据依然存在!

这是为何呢?
其实我上面用了两个router-view维护了两种组件渲染方式。也就是说点击tag标签时,所有页面组件都是keep-alive的,点击菜单刷新时,所有页面都是正常渲染的,他们彼此间没法互相交互。

我的困扰点

其实,我困扰的点主要是如何去动态改变组件keep-alive状态,即a页面通过点击tag标签渲染的话,需要加入keep-alive,但是通过菜单渲染的话要取消keep-alive状态!

这样看有点destroy的意思,但是我不可能在几百个vue组件中去destroy!

解决过程

百思不得其解后,试着百度了下,偶然在segmentfault看到个评论:
使用keep-aliveinclude

我立马去官方文档上查看该属性(以前从没用过):https://cn.vuejs.org/v2/api/#keep-alive

当我看到可以通过v-bind去动态改变include时,心中顿时觉得有希望了,不过我需要在之前的route中继续维护组件名称属性——compontName,同时需要在每个路由对应的组件中把name加上,然后测试了下,果然好了!

附上代码:

// main.vue
<keep-alive :include="includes">
  <router-view class="fadeInRight animated"></router-view>
</keep-alive>
// mixins handleTags
import route from '@/config/route'
const handleTags = {
  data () {
    return {
      tags: [], // 缓存所有打开过的菜单页并用tag展示
      includes: [], // 缓存需要keep-alive的组件名,点击菜单刷新页面时从中移除
    }
  },
  mounted() {
    const path = this.currentPath
    const tags = this.tags
    !tags.length && this.addTag(path)
  },
  computed: {
    /**
     * @description 获取当前页面路由
     */
    currentPath () {
      return this.$route.path
    }
  },
  methods: {
    /**
    * @description 选择左侧菜单触发,用于渲染右侧头部tags标签以及去除相应页面的keep-alive
    */
    selectMenu ({index, indexPath}) {
      const i = this.findIndex(this.tags, index, 'path')
      if (index && index.indexOf('/') !== -1 && i === -1) {
        this.addTag(index)
      }
      this.spliceInclude(index)
    },
    /**
     * 
     * @param {*} arr
     * @param {*} path
     * @description 判断该路由是否已经存在 
     */
    findIndex (arr, value, property) {
      return arr.findIndex(item => {
        return property ? item[property] === value : item === value
      })
    },
    /**
     * 
     * @param {*} path
     * @description 添加标签 
     */
    addTag (path) {
      const tags = this.tags
      route.some(item => {
        item.path === path && tags.push(item)
      })
    },
    /**
     * @description 点击tag触发。将所有的tag标签对应的路由组件全部加入keep-alive
     */
    handleClickTag (tag) {
      let componentName
      const tags = this.tags
      tags.forEach(item => {
        componentName = this.getComponentName(item.path)
        !this.includes.includes(componentName) && this.includes.push(componentName)
      })
      this.$nextTick(() => {
        this.$router.push(tag.path)
      })
    },
    /**
     * 
     * @param {*} tag
     * @description 关闭tag触发。
     *              如果只剩一个不给删除;
     *              删除当前tag默认展示前一个tag对应的页面;
     *              如果前面没有tag了,默认展示当前第一个tag;
     *              同时从keep-alive中删除;
     */
    handleClose (tag) {
      const index = this.findIndex(this.tags, tag.path, 'path')
      this.tags.splice(index, 1)
      if (this.currentPath === tag.path) {
        const next = index - 1 >= 0 ? index - 1 : 0
        this.$router.push(this.tags[next].path)
      }
      this.spliceInclude(tag.path)
    },
    /**
     * @description 获取当前路由对应的组件名称
     */
    getComponentName (path) {
      // 获取在route配置中的位置
      const j = this.findIndex(route, path, 'path')
      if (j !== -1) {
        return route[j].componentName
      }
      return
    },
    /**
     * @description 从keep-alive状态的组件数组中删除
     */
    spliceInclude (path) {
      const componentName = this.getComponentName(path)
      const includes = this.includes
      const k =  this.findIndex(includes, componentName)
      k !== -1 && this.includes.splice(k, 1)
    }
  },
}
export default handleTags

// route.js
const route = [
  {
    name: 'a',
    path: '/a',
    componentName: 'a'
  },
  {
    name: 'b',
    path: '/b',
    componentName: 'b'
  },
  {
    name: 'c',
    path: '/c',
    componentName: 'c'
  },
]

vue-router从history模式切换到hash模式踩坑

背景

前段时间做了个单点登录,就是两个系统带上token来回跳。

本来已经OK了,结果昨天冬哥说测试环境上服务监督页的操作按钮都点不了,我看了下测试环境控制台,报错:Unexpected token <

研究过程

一开始我以为是代码里面问题,本地启动测试,没问题,那就排除。

遂百度,百度了好几篇文章,都说要么是nginx配置问题,要么就是路由用的history模式并且生产环境的assetsPublicPath配置了 ./

nginx配置这个问题,冬哥在第一次部署时就发现并解决了。

再看webpack配置,果然assetsPublicPath改成了 ./,我记得是为了解决线上环境没有icon图标才这样改的。那么这里最好不要动。

再去看看router配置,果然用的是history模式。我没多想,把它注释掉提交给冬哥测试。结果,测试环境上没法登陆了,一直在两个系统之间来回跳。也就是之前的单点登录功能突然出问题了。这就真的很奇怪了。

我通过chrome浏览器使用slow 3G模式,放缓跳转时间,发现浏览器url栏是对的,但是,路由拦截器打印的却和url完全不同!!!

看图:

路由监听代码:

// 针对单点登录进行路由拦截
  router.beforeEach(async (to, from, next) => {
    console.log(to, from)
    // 拿到当前用户在浏览器中的信息,没有token且启用单点登录,则跳转到养老系统去登录拿token
    let userinfo = storage.get('userinfo') || {}
    // 这里有可能是养老系统登录后通过callback并携带token跳回来,需要拿到token避免下面再去重复跳养老
    const tokenIndex = to.fullPath.indexOf('token')
    if (tokenIndex !== -1) {
      const token = to.fullPath.substring(tokenIndex + 6)
      // 根据养老系统返回的token去查本系统用户信息和本系统对应的token
      // JSON.parse,不要直接赋值
      storage.set('userinfo', {token: token})
      const result = await getUserInfoByToken({
        params: {
          token: token
        }
      })
      storage.set('userinfo', result.payload)
      userinfo = storage.get('userinfo')
    }
    console.log(userinfo)
    // 启用单点登录但是没有token
    if (singleSignOn && !userinfo.token) {
      // 退出登录时清空token,跳到志愿者系统时告诉志愿者系统也要清空token
      const tokenError = storage.get('tokenError')
      storage.set('tokenError', false)
      window.location.href = tokenError ? `${singleSignUrl}?callback=${callbackUrl}&tokenError=true` : `${singleSignUrl}?callback=${callbackUrl}`
      return
    }
    // 启用单点,有token并且准备去login页,直接进home
    if (singleSignOn && userinfo.token && to.path.indexOf('login') !== -1) {
      next('/home')
      return
    }
    next()
  })

仔细观察上图,就会发现,明明浏览器url是home,但是控制台打印的确是//login,这怎么回事???

我真的懵逼了。之前在本地根本没有这样的问题啊!

由于昨天晚上要聚餐,我也没有什么思路,就出去聚餐了。

结果,晚上冬哥,这个有着10年后端开发经验的经理级别大哥,晚上愣是在家调试了2个多小时,一直到接近12点,当时我刚回家洗完澡。。。

本来他也要放弃了,但是,他看了我的提交代码,抱着试一试的心态,把我注释的history给改回来,结果,我日,OK了。

这居然就OK了!!!

然后他把url复制出来,比对一下,发现,注释掉history后,url变成了
http://localhost:8088/home?token=eyJhbGciOiJIUzI1NiIsInppcCI6IkRFRiJ9.eNocjMEKwjAQBf9lzw24yWZje_cmKIrnsmk2EEEtpgVB_Hejxze8mTfUNcIA58sROlirPseS2rbBco4xGcbAhrIkI2yjEaTsRNATUxPq9Ji13U-H_W6UdCv3BvU1w4Ce2dot9dxBkeUPPFvHP3BdSpMCJlXWqUWxN-Q2ZHpnnQneKWrOMacJPl8AAAD__w.OmL3aBhN-haWanxdPl0jicWvf3FwDgtc7EAusGqEIs0/#/

而使用history模式,url则是:
http://localhost:8088/#/home?token=eyJhbGciOiJIUzI1NiIsInppcCI6IkRFRiJ9.eNocjMEKwjAQBf9lzw24yWZje_cmKIrnsmk2EEEtpgVB_Hejxze8mTfUNcIA58sROlirPseS2rbBco4xGcbAhrIkI2yjEaTsRNATUxPq9Ji13U-H_W6UdCv3BvU1w4Ce2dot9dxBkeUPPFvHP3BdSpMCJlXWqUWxN-Q2ZHpnnQneKWrOMacJPl8AAAD__w.OmL3aBhN-haWanxdPl0jicWvf3FwDgtc7EAusGqEIs0

看到区别了吗???

不使用history模式,那么我在志愿者系统直接通过window.location.href跳转过来时,他会默认在我url最后加个 /#/,这样的话实际路由它会认为是/,然后重定向到/login,而康养系统的/login是要跳到志愿者系统的,所以,这样不停的来回跳转!!!

最终解决

  1. 依然使用history模式,其他什么都不用动,那么单点登录没问题,但是某些页面操作按钮报错需要想办法解决。
  2. 换成hash模式,并把跳转地址改成window.location.href='http://localhost:8088/#/home?token=你的token'

vue的transition组件学习以及为何与display搭配能起到过渡效果的个人理解

起因

  昨天项目中用到了下拉列表功能,我抄的iview的,功能没问题,就是这个过渡效果令我很奇怪,明明大部分代码都是抄的,怎么iview那么动感,我这里这么生硬。我注意到iview的Dropdown组件是通过v-show来控制的,根据vue官方文档单元素/组件的过渡说明,这里也没有问题啊。

  但是我还是留了心眼,v-show明明改变的是display属性,我记得css里面transition属性是不支持display的,怎么这里就有效果呢?

  我不放心的去MDN查了下,我的记忆是对的,transition的确不支持display属性。具体可见可动画属性列表

  那它到底用了什么*操作呢?谷歌浏览器控制台也没看出什么啊?记得以前用iview开发时就有这个困惑,这个困惑必须得解开,不然心里不踏实!

水。。。

  我双12一天都在研究transition组件代码,从刚开始一脸懵逼到渐渐找到门路,只是由于之前只看过vue核心双向绑定那块的代码,什么VNode之类的几乎没看,以前是真的菜,看不懂,今天强行看了一波,勉强看懂30%,实在不行只好去百度查vue源码解析相关,就在此时,我突然想起来很久很久以前,我,好像,曾经,给好几个,分析vue源码的库,点过star。。。
  这是病,明明点过很多star,却都不怎么看!当我找到那些库时,有两个极为牛逼,我自愧不如,其中一个就有transition组件等的讲解,看完我能明白7成吧。这时,我知道,我不该开这个issue的,自己都不完全懂就敢狂妄的写这个标题,实在班门弄斧!!!
以下是那两个极为牛逼的vue源码分析:

  想看transition分析的可以看滴滴员工的那个链接,但是强烈建议全都从头看一遍,真的学到很多!

个人探索

这里的探索主要依赖于自己copy iview的Dropdow组件源码以及看完滴滴员工的vue源码分析所得。

主要看这里:https://ustbhuangyi.github.io/vue-analysis/extend/tansition.html#%E6%80%BB%E7%BB%93 ,最后一句话:
所以真正执行动画的是我们写的 CSS 或者是 JavaScript 钩子函数,而 Vue 的 只是帮我们很好地管理了这些 CSS 的添加/删除,以及钩子函数的执行时机。

也就是说,我以前非常错误的认为这些动态过渡效果是vue提供的,极度错误的,真丢脸啊,无地自容!!!

那么,iview这么炫酷的动态怎么做的呢?
其实靠的还是css动画以及scaleYtransform-origin属性!!!
iview dropdown动态效果部分css源码:https://github.com/iview/iview/blob/2.0/src/styles/animation/slide.less#L12

反省

我太菜了。。。

手写一个假冒伪劣Promise会不会被喷?

myPromise

2019-12-12修改

时隔一年多,回顾头来自己再写一次,巩固下,发现同样的this丢失问题依然困扰了我一会,基础还是不够牢固。

最重要的是,我发现先我以前模拟的Promise居然是错的!!!

then 方法内部用的居然是 同步操作。。。OMG,我一直没意识到,直到今天回顾才发现,卧槽,为何人家 then 里面用 setTimeout???

再结合经典面试题

new Promise(resolve => {
  console.log(1)
  resolve(2)
}).then(res => console.log(res))
console.log(3)

// 打印输出是 1 3 2

then 里面可不就是个微任务嘛。。。(但是setTimeout是宏任务,网上大多用它来实现Promise。但其实两者是有区别的!)
PS:js里面有宏任务和微任务之分,vue源码中的nextTick也涉及这方面知识。可以看Tasks, microtasks, queues and schedules(宏任务与微任务)了解。

宏任务:

微任务:

尼玛,真尴尬啊!!!

果断把以前的内容给删了,这么尴尬的事情怎么能留下来呢!

阅读

重学js —— Lexical Environments(词法环境)和 Environment Records(环境记录)

Executable Code and Execution Contexts

Lexical Environments(词法环境)

Lexical Environments是一种规范类型,用于根据ECMAScript代码的词法嵌套结构定义标识符与特定变量和函数的关联。一个词法环境由一个Environment Records和一个可能为空的外部词法环境的引用组成。通常,词法环境与ECMAScript代码的特定句法结构有关。例如函数申明块语句try语句中的catch等代码每次运算后会产生新的词法环境。

一条Environment Records记录在其关联词法环境作用域内创建的标识符绑定。它指向词法环境的EnvironmentRecord

PS:便于理解,可以把词法环境间的嵌套看成 树结构

外部环境引用用于模拟词法环境值的逻辑嵌套。内部词法环境的外部引用 是对一个逻辑上围绕内部词法环境的词法环境的引用(有点绕人)。外部词法环境也会有它自己的外部词法环境(就像树形结构一样,子父级关系)。一个词法环境可以作为多个内部词法环境的外部环境(类似有多个子级)。

全局环境 是一个没有外部词法环境的词法环境(看作树的根节点)。全局环境的外部环境引用是 null。全局环境的EnvironmentRecord可能预填充了标识符绑定,并包括一个关联的全局对象,该对象的属性提供了某些全局环境的标识符绑定。当ECMAScript代码执行时,可能会对全局对象增加额外的属性并且初始属性可能会被修改。

module 环境是一个包含了模块顶级申明绑定的词法环境。 它还包含模块显式引入的绑定。module 环境的外部环境是全局环境。

function环境是一个对应于ECMAScript函数对象调用的词法环境。函数环境可能建立一个新的 this绑定。函数环境还捕获支持 super 方法调用所需的状态。

Lexical Environments 和 Environment Record 的值是纯规范机制并且不需要与任何特定的ECMAScript实现对应。ECMAScript程序是无法直接获取或操作它们的值的。

Environment Records

规范中有两种主要的Environment Records值:declarative Environment Recordsobject Environment Records

声明性环境记录用于定义ECMAScript语言语法元素(例如FunctionDeclarationsVariableDeclarationsCatch)的效果,这些元素直接将标识符绑定与ECMAScript语言值相关联

对象环境记录用于定义ECMAScript元素(例如WithStatement)的效果,这些元素将标识符绑定与某些对象的属性相关联。

全局环境记录和函数环境记录专门用于脚本全局声明和函数内顶级声明。

出于规范目的,环境记录值是Record规范类型的值并且可以认为它存在于简单的面向对象的层次结构中,其中Environment Record是具有三个具体子类的抽象类,分别是declarative Environment Record, object Environment Record, 和 global Environment Record。

Function Environment Records 和 module Environment Records是declarative Environment Record子类。这些抽象类包含一些抽象的方法定义在下面表格中:

方法 目标
HasBinding(N) 确定环境记录是否具有字符串值N的绑定。有的话返回true,否则返回false。
CreateMutableBinding(N, D) 在环境记录中创建一个新的但是未初始化且可变的绑定。字符串值N是绑定名称。如果布尔参数D是 true,那么这个绑定随后可能被删除。
CreateImmutableBinding(N, S) 在环境记录中创建一个新的但是未初始化且可变的绑定。字符串值N是绑定名称。如果S是 true,对其进行初始化之后进行设置,无论引用该绑定的操作的严格模式设置如何,都将始终引发异常。
InitializeBinding(N, V) 在环境记录中对一个已存在但未初始化的绑定设置值。字符串值N是绑定名称。V是绑定的值并且该值符合ECMAScript语言类型。
SetMutableBinding(N, V, S) 在环境记录中对一个已存在且可变的绑定设置值。字符串值N是绑定名称。V是绑定的值并且该值可能符合ECMAScript语言类型。S是一个布尔标志。如果S是true并且不能设置绑定则抛出一个类型错误。
GetBindingValue(N, S) 从环境记录中返回一个已存在绑定的值。字符串值N是绑定名称。S用于标识源自严格模式代码或其他需要严格模式引用语义的引用。如果S是true并且绑定不存在,抛出一个引用错误。如果绑定存在但未初始化,则无论S的值如何,都会引发ReferenceError。
DeleteBinding(N) 从环境记录中删除绑定。字符串值N是绑定名称。如果N的绑定存在,移除绑定并返回 true。如果绑定存在但不能被移除,返回 false。如果绑定不存在则返回 true
HasThisBinding() 取决于环境记录是否建立一个 this 绑定。如果有返回 true 否则返回 false
HasSuperBinding() 取决于环境记录是否建立一个 super 绑定。如果有返回 true 否则返回 false
WithBaseObject() 如果环境记录关联了一个 with 语句,返回 with 对象。否则,返回undefined

声明性环境记录(Declarative Environment Records)

每个声明性环境记录都与包含变量(var),常量(const),let,类(class),模块(module),导入(import)和函数声明(function)的ECMAScript程序作用域相关联。声明性环境记录绑定在其作用域内声明的标识符集。

SetMutableBinding ( N, V, S )方法,在某些很罕见的情况下没有存在的绑定,例如:

function f() { eval("var x; x = (delete x, 0);"); }

对象环境记录(Object Environment Records)

每个对象环境记录与其绑定对象相关联。对象环境记录绑定 直接与其绑定对象的属性名称 相对应的一组字符串标识符名称。不是以IdentifierName形式出现的字符串的属性键不包含在绑定标识符集中。自有以及继承的属性都会被包含在该集合内,无论它们的[[Enumerable]]属性设置。由于对象属性可以动态增加和删除,对象环境记录所绑定的标识符集可能会因添加或删除属性的任何操作的副作用而发生更改。由于这种副作用而创建的任何绑定都被视为可变绑定,即使相应属性的Writable属性的值为 false 也不例外。对象环境记录中不存在不可变绑定。

with 语句创建的对象环境记录可以提供它们的绑定对象作为一个隐式 this 值在函数调用中使用。该功能由与每个对象环境记录关联的withEnvironment布尔值控制。 默认情况下,对于任何对象环境记录,withEnvironment的值为false

Global Environment Records

全局环境记录用于表示在共同领域中处理的所有ECMAScript脚本元素共享的最外部作用域。全局环境记录为内置全局变量,全局对象的属性以及脚本中发生的所有顶级声明(13.2.813.2.10)提供了绑定。

逻辑上来说,全局环境记录是单个记录,但是它被指定为封装对象环境记录和声明性环境记录的 复合记录。对象环境记录将关联领域记录的全局对象作为其基对象。此全局对象是全局环境记录的GetThisBinding方法返回的值。全局环境记录的对象环境记录包含所有内置全局变量的绑定(第18节)以及全局代码中包含的FunctionDeclarationGeneratorDeclarationAsyncFunctionDeclarationAsyncGeneratorDeclarationVariableStatement引入的所有绑定。全局代码中所有其他ECMAScript声明的绑定包含在全局环境记录的声明性环境记录部分中。

可以直接在全局对象上创建属性。因此,全局环境记录的对象环境记录组件可能包含由FunctionDeclarationGeneratorDeclarationAsyncFunctionDeclarationAsyncGeneratorDeclarationVariableStatement声明显式创建的绑定,以及隐式创建为全局对象属性的绑定。为了识别使用声明显式创建的绑定,全局环境记录使用其CreateGlobalVarBinding和CreateGlobalFunctionBinding具体方法维护绑定名称的列表。

全局环境记录有额外的字段列表:

字段名 含义
[[ObjectRecord]] 对象环境记录 绑定的是全局对象。它在相关领域的全局代码中包含全局内置绑定以及FunctionDeclaration,GeneratorDeclaration,AsyncFunctionDeclaration,AsyncGeneratorDeclaration和VariableDeclaration绑定。
[[GlobalThisValue]] Object 全局作用域内返回this。主机可以返回任何ECMAScript对象值。
[[DeclarativeRecord]] 声明性环境记录 包含相关领域代码的全局代码中所有声明的绑定,但FunctionDeclaration,GeneratorDeclaration, AsyncFunctionDeclaration, AsyncGeneratorDeclaration, 和 VariableDeclaration 除外。
[[VarNames]] 字符串列表 在关联领域的全局代码中,由FunctionDeclaration,GeneratorDeclaration,AsyncFunctionDeclaration,AsyncGeneratorDeclaration和VariableDeclaration声明绑定的字符串名称。

Function Environment Records

Function 环境记录是一个声明性环境记录,用于表示函数的顶级作用域并且如果该函数不是一个箭头函数,则会提供一个 this 绑定。如果该函数不是箭头函数且有 super 引用,它的function环境记录还包含用于从function内部执行 super 方法调用的状态。

下面是函数环境记录额外的字段列表:

字段名 含义
[[ThisValue]] any 该函数调用时的 this
[[ThisBindingStatus]] lexical 或 initialized 或 uninitialized 如果值为lexical,则是箭头函数,没有this
[[FunctionObject]] Object 调用导致创建此环境记录的函数对象
[[HomeObject]] Object 或 undefined 如果关联的函数具有 super 属性访问权限,并且不是箭头函数,那么[[HomeObject]]是函数作为方法绑定到的对象。默认值是undefined
[[NewTarget]] Object 或 undefined 如果环境记录由[[Construct]]内置方法创建,则[[NewTarget]]是[[Construct]]参数newTarget的值。否则,它的值为undefined

Module Environment Records

module 环境记录是一个声明性环境记录,用于表示ECMAScript Module的外部作用域。此外,对于通常可变以及不可变的绑定,module 环境记录也会提供不可变的导入绑定(提供对另一个环境记录中存在的目标绑定的间接访问的绑定)。

词法环境是不是平常理解的作用域???

不全是。说环境记录是作用域可能更准确。
词法环境更像是作用域链。

2020-07-27 补充

来自高级前端面试小程序js基础第9题

var a = 10;
(function() {
  console.log(a); // undefined
  a = 5;
  console.log(window.a); // 10
  var a = 20;
  console.log(a); // 20
})()

读《webkit技术内幕》了解浏览器内核六——硬件加速机制

前面了解到浏览器有软件渲染和GPU硬件加速渲染以及软件绘图的合成化渲染等方式,其中硬件加速渲染算是比较重要的,不过我只是了解了它的一些优缺点,至于浏览器到底如何实现硬件加速机制的,我还不知道,所以,本篇继续跟着书中讲解来了解下硬件加速机制

硬件加速机制其实就是指使用GPU的硬件能力来帮助渲染网页。

依然采用问题形式。

问题

前一篇提到过,在分层的网页结构中,GPU硬件加速渲染会提供相应的后端存储,而网页往往较为复杂,可能会分成很多层,会有很多 RenderLayer 对象,那么GPU是不是要给所有的RenderLayer 对象都进行后端存储?

那是理想情况,现实中不一定。

主要原因是实际中的硬件能力和资源有限。

考虑到资源节省等问题,硬件加速机制是如何去优化、去渲染网页呢?

主要是三件事:

  • WebKit决定将哪些 RenderLayer 对象组合在一起,形成一个有后端存储的新层,这一新层不久后会用于之后的 合成,作者将其称为 合成层。每个新层都有一个或多个后端存储,这里的后端存储可能是 GPU的内存。对于一个 RenderLayer 对象,如果它没有后端存储的新层,那么就使用它父亲所使用的的 合成层。
  • 将每个合成层包含的这些 RenderLayer 内容绘制在 合成层 的 后端存储 中,这里的绘制可以是软件绘制也可以是硬件绘制。
  • 合成器 将多个合成层合成起来,形成网页的最终可视化结果,实际就是一张图片。合成器 是一种能够将多个合成层按照这些层的前后顺序、合成层的 3D 变形等设置而合成一个图像结果的设施。

那么哪些 RenderLayer 对象可以组合成 合成层 呢?

具有以下等特征:

  • RenderLayer具有CSS 3D属性或者CSS透视效果
  • RenderLayer包含的RenderObject节点表示的是使用硬件加速的视频解码技术的 HTML5 video 元素
  • RenderLayer包含的RenderObject节点表示的是使用硬件加速的 Canvas 2D元素或者WebGL技术
  • RenderLayer使用了CSS透明效果的动画或者CSS变换的动画
  • RenderLayer使用了硬件加速的CSS Filters技术
  • RenderLayer使用了裁剪(Clip)或者反射(Reflection)属性,并且它的后代中包括一个合成层
  • RenderLayer有一个Z坐标比自己小的兄弟节点,且该节点是一个合成层

了解了以上的知识后,能说说硬件渲染的过程吗?

由于涉及到不少内部类和方法,考虑到项目一直在迭代,可能书上所讲的有所改变,所以这里提炼下大致过程,尽量不涉及具体类和方法名。

1.首先WebKit确定并计算合成层

  • 检查 RenderLayer 对象是否为合成层,是的话为其创建 后端存储对象RenderLayerBacking
  • 根据重新更新的合成层来更改合成层树,并修改后端存储对象的设置信息

2.其次,遍历和绘制每一个合成层,可能存在以下四种情况:

  • 第一种情况:HTMLDocument节点, 需要一个用于2D图形的 图形上下文对象,同软件渲染非常类似,但递归过程不同。每个 RenderLayer 对象被绘制到 祖先链中最近的合成层
  • 第二种情况:使用 CSS 3D变形的合成层。
  • 第三种情况:使用WebGL技术的 Canvas 元素所在的合成层,它的绘制由javascript操作完成,并且使用了 3D图形上下文
  • 第四种情况:类似使用了硬件加速的视频元素所在的合成层,该层的内容其实是由视频解码器来绘制,而后通过定时器或者其他通知机制来告诉WebKit该层内容发生改变.

3.最后渲染引擎将所有绘制完的合成层合成起来

前面介绍到webkit中会有3D和2D图形上下文,Chromium是不是也一样呢?内部调用栈是什么样的呢?

webkit中2D图形上下文对应Chromium的 Skia画布(canvas),具体调用栈可以看图了解下即可。

结合以前介绍的进程灯知识,GPU硬件加速渲染是在哪个进程上执行的?是Renderer进程吗?

对于使用多进程模型的Blink内核的浏览器来说,不是的,GPU硬件加速渲染的操作会交由 GPU进程 负责。

多进程模型下是如何实现跨进程的硬件加速渲染的?

这个很复杂,依靠Renderer进程的主要类和GPU进程的主要类它们彼此内部的实现。涉及到太多类,不展开,建议结合项目代码去看。

总结就是,GPU进程处理一些命令后,会向Renderer进程报告自己当前状态,Renderer进程通过检查状态信息和自己期望结果来确定是否满足自己的条件。

值得注意的是:GPU进程最终绘制的结果不再像 软件渲染那样通过共享内存传递给Browser进程,而是直接将页面内容绘制在浏览器的标签窗口。

本篇第一个问题下的回答就提到过合成器,Chromium的合成器是如何实现的呢?

在架构设计上,采用了 表示和实现分离原则
图例:

需要注意:Layer树工作的主线程,实际指的是渲染引擎工作的线程,不一定是Renderer进程的主线程。但是LayerImpl树工作的 实现部分的线程 既可以是主线程也可以是单独的线程。实现部分如果是一个单独的线程,那么可以称为 合成器线程,也叫线程化合成

Chromium合成器的组成部分是什么样的?

  • 事件处理部分。主要接收WebKit或者其他的用户事件,例如网页滚动、放大缩小等事件,这些事件会请求合成器重新绘制每一个合成层,然后合成器再合成这些层的绘制结果
  • 合成层的表示和实现。主要定义各种类型的合成层,包括它们的位置、滚动位置、颜色等属性
  • 合成层组成两种类型的树,以及它们之间的同步等机制
  • 合成调度器(Scheduler)主要调度来自用户的请求,它包括一个状态用于调度当前队列中需要执行的请求,目的当然是协调合成层的绘制和合成、树的同步等操作。调度器是在合成器线程中的,因而不能访问主线程中的资源
  • 合成器的输出结果。结果可以是一个GPU Surface或者一个CPU存储空间。
  • 各种后端存储等资源
  • 支持动画和3D变形这些功能所需要的基础设施

拓展介绍:绘制HTML元素和图片元素所生成的合成层,其后端存储会被瓦片化!

为什么会使用瓦片化的后端存储呢?

有以下三点好处:

  1. DOM树中的html元素所在的层可能会比较大,因为网页高度很大,如果只是使用一个后端存储的话,那么需要一个很大的对象,但是实际的GPU硬件可能只支持非常有限的纹理大小
  2. 在一个比较大的合成层中,可能只是其中一部分发生变化,根据之前的介绍,需要重新绘制整个层,这样必然产生额外的开销,使用瓦片化的后端存储,就只需要重绘一些存在更新的瓦片
  3. 当层发生滚动时,一些瓦片可能不再需要,然后WebKit需要一些新的瓦片来绘制新的区域,这些大小相同的后端存储很容易重复利用,简洁漂亮

了解完合成器知识后,能简要说说合成过程吗?

主要有四个步骤,都是由调度器调度:

  1. 创建输出结果的目标对象 Surface,也就是合成结果的存储空间
  2. 开始一个新的帧,包括计算滚动和缩放大小、动画计算、重新计算网页的布局、绘制每个合成层等
  3. 将Layer树中包含的这些变动同步到LayerImpl树中
  4. 合成LayerImpl树中各个层并交换前后帧缓冲区,完成一帧的绘制和显示动作

可以看看图了解下

前面介绍过2D绘图也可以进行硬件加速,如何做到的呢?

首先什么是2D绘图的硬件加速?

2D绘图本身是使用 2D的图形上下文,一般使用软件方式来绘制它们,也就是 光栅化

使用GPU来绘制2D图形的方法称为2D图形的硬件加速机制。

2D图形的硬件加速机制有两种应用场景:

  1. 2D图形上下文——网页基本元素绘制(HTML基本标签)
  2. canvas 2D

2D图形上下文是如何进行硬件加速的呢?

依靠 Skia图形库。当需要启动硬件加速时,只需要对 SkCanvas 对象进行的相应设置即可。

具体内部类和方法建议看书和代码。

Canvas 2D是如何进行硬件加速的呢?

比较复杂,涉及太多内部类,建议结合项目代码和书去看。
简单来说,在Chromium中,canvas 2D进行GPU硬件加速绘图时,会创建一个 SkDeferredCanvas 对象,该对象采用 延迟机制 来绘制2D图形。然后,该对象需要 SkGpuDevice 来将2D绘图操作转换为使用3D图形上下文来绘制。

WebGL是如何实现的?

同样设计太多内部类,建议看书。
粗略提下吧,Chromium中WebGL工作过程分成三个阶段:

  1. 对象的初始化
  2. 构建RenderLayer、WebLayer、CC::Layer等对象,在DOM树构建之后检查CSS样式变化时才会触发
  3. 3D绘图

读《webkit技术内幕》了解浏览器内核系列

  1. 读《webkit技术内幕》了解浏览器内核一
  2. 读《webkit技术内幕》了解浏览器内核二——内部架构
  3. 读《webkit技术内幕》了解浏览器内核三——资源与网络栈
  4. 读《webkit技术内幕》了解浏览器内核四——HTML、DOM以及CSS相关
  5. 读《webkit技术内幕》了解浏览器内核五——渲染基础
  6. 读《webkit技术内幕》了解浏览器内核六——硬件加速机制
  7. 读《webkit技术内幕》了解浏览器内核七——js引擎

重学js —— 规范中的Agents阅读

Agents

主要是对规范翻译。

代理包括一组ECMAScript执行上下文,一个执行上下文栈,一个运行时执行上下文,一组命名任务队列,一个 代理记录 和一个执行线程。除执行线程外,代理的组成部分完全属于该代理。

代理程序的执行线程独立于其他代理程序,在代理程序的执行上下文上执行代理程序任务队列中的任务。除非共享线程中没有一个代理具有[[CanBlock]]属性为 true 的代理记录,否则执行线程可以被多个代理使用。

例如,某些Web浏览器跨浏览器窗口的多个不相关的选项卡共享一个执行线程。

当代理的执行线程执行代理任务队列中的任务时,该代理是这些任务中代码的 周围代理。这些代码使用周围的代理访问该代理内保存的规范级别执行对象:运行时执行上下文执行上下文栈,命名的任务队列以及代理记录的字段。

下表列出代理记录的字段:

原子操作是指,对内存的一系列的读写操作必须保证不被打断来保证最终结果的正确性。可以去wiki上搜,不能翻墙的见百度

字段名 意义
[[LittleEndian]] Boolean类型 使用算法GetValueFromBufferSetValueInBuffer为其计算默认值。选择取决于实现,并且应该是对实现最有效的选择。一旦观察到该值,就无法更改。
[[CanBlock]] Boolean类型 确定代理是否可以阻止。
[[Signifier]] 任何全局唯一值 用于表示该代理在代理群内的唯一标识。
[[IsLockFree1]] Boolean类型 如果对单字节值的原子操作是无锁的,则为 true,否则为 false
[[IsLockFree2]] Boolean类型 如果对2字节值的原子操作是无锁的,则为 true,否则为 false
[[IsLockFree8]] Boolean类型 如果对8字节值的原子操作是无锁的,则为 true,否则为 false
[[CandidateExecution]] 一个candidate execution记录 内存模型

一旦代理群中的任何代理观察到[[Signifier]][[IsLockFree1]][[IsLockFree2]]的值,它们就无法更改。

[[IsLockFree1]][[IsLockFree2]]的值不一定由硬件确定,但也可能反映了ES实现选择,这些选择会随着时间的推移以及ECMAScript实现之间的变化而变化。

没有[[IsLockFree4]]属性:4字节原子操作始终是无锁的。

实际上,如果用任何类型的锁来实现原子操作,则该操作不是无锁的。无锁并不意味着无等待:完成无锁原子操作可能需要多少机器步骤没有上限。

大小为n的原子访问是无锁的,并不意味着有关大小为n的非原子访问的(感知)原子性,特别是,非原子访问仍可以作为几个单独的内存访问的序列来执行。详见ReadSharedMemoryWriteSharedMemory

代理是一种规范机制,不需要与ECMAScript实现的任何特定工件相对应。

Agent Clusters(代理集群)

代理程序集群是 通过对共享内存进行操作而通信的最大代理程序集。

不同代理中的程序可能通过未指定的方式共享内存。 至少,可以在集群中的代理之间共享SharedArrayBuffer对象的后备内存。

可能有一些代理可以通过消息传递进行通信,而这些消息不能共享内存。 它们永远不在同一个代理集群中。

每个代理都完全属于一个代理集群。

集群中的代理不必在某个特定时间点都处于活动状态。如果代理A创建了代理B,之后,A终止,B又创建代理C,如果A可以与B共享一些内存并且B也可以与C共享一些内存,则A,B,C三个代理程序在同一个集群中。

同一个集群内的所有代理程序在各自 代理记录 内必须具有 相同[[LittleEndian]]值。

注意:如果一个代理集群内不同的代理程序有不同的[[LittleEndian]]值,则很难将共享内存用于多字节数据。

集群内的所有代理在各自的 代理记录 中必须有 相同[[IsLockFree1]] 值;[[IsLockFree2]]属性和它类似。

集群内的所有代理在各自的 代理记录 中必须有 不同[[Signifier]]值。

在代理不知情或不合作的情况下,嵌入可能会使其无效(停止前进)或激活(继续前进)。如果嵌入这样做,则一定不能使集群中的某些代理处于活动状态,而集群中的其他代理会被无限期停用。

前述限制的目的是避免由于另一个代理已被停用而导致代理死锁或饥饿的情况。例如,如果一个HTML共享工作进程的生命周期独立于任何窗口中的文档,则允许它与此类独立文档的专用工作进程共享内存,并且文档及其专用工作线程将被停用,而专用工作线程持有一个锁(例如,文档将被推入其窗口的历史记录中),然后,共享工作进程尝试获取锁,然后将阻塞共享工作进程,直到再次激活专用工作进程(如果有的话)。与此同时,试图从其他窗口访问共享工作进程的其他工作进程将会失败。

这一限制的含义是,嵌入中不属于同一 挂起/唤醒 集合的代理之间将无法共享内存。

嵌入可以在没有任何代理的集群的其他代理事先知道或合作的情况下终止代理。如果某个代理不是通过其自身或集群中其他代理的编程动作而是通过集群外部的力量终止的,那么嵌入必须选择两种策略之一:

  • 要么终止集群中的所有代理,
  • 提供可靠的API,允许集群中的代理进行协调,以便至少还有一个集群成员能够检测到终止,终止数据包含足够的信息来标识已终止的代理。

此类终止的示例包括:操作系统或用户终止在不同进程中运行的代理;当每个代理程序资源记帐指示该代理程序失控时,嵌入本身将终止与其他代理程序一起在进程中运行的代理程序。

在集群中的任何代理对任何ECMAScript代码进行任何运算之前,将集群中所有代理的 代理记录 的[[CandidateExecution]]字段设置为初始候选执行。初始候选执行是一个空的候选执行,它的[[EventsRecords]]字段是一个List,对于每个代理,该列表包含一个 代理事件记录,该代理事件记录的[[AgentSignifier]]字段是该代理的标识符,并且其[[EventList]][[AgentSynchronizesWith]]字段为空列表。

代理群集中的所有代理在其代理记录[[CandidateExecution]]字段**享相同的候选执行候选执行是一种被内存模型使用的规范机制。

读《你不知道的javascript》上卷—原型和行为委托

前言

最近一段时间没怎么登github,公司内部走代理访问github打开特别慢,排版布局又很乱,据悉是公司内部防火墙的缘故。
晚上回来也迟,就没有打开电脑去看搜藏的博客。不过不要紧,毕竟也买了不少技术书,那就静下心来看看书吧。
PS:《你不知道的javascript》上中两卷买回来起码半年了,昨天才看完上卷,太懒了!!!得逼自己一把,不然怎么快速进步!!!
看书之前以为我都会,看完之后才发现我还不够!
尤其看完这两章,我对原型链以及js继承又有了全新的认识,这里特地把自己以前没有学习到的新知识记录下来,做个总结。
建议大家去看该系列书籍,github上有英文版:《你不知道的javascript》

get到的新点

1.属性设置和屏蔽

这里主要讲了当对象(例如myObj)与它的原型对象(例如obj)上都存在同名属性(例如foo)时,myObj.foo会屏蔽掉它原型对象上的同名foo属性(这里指obj.foo)。即当进行get操作时得到的是myObj.foo,这点大家都清楚。

但是!如果myObj中没有foo属性,而它的原型对象obj上有foo属性,那么当进行 myObj.foo= 赋值时,情况可能并不是我们想当然的那样,只在myObj中新增一个foo属性!

这里有个专业术语:屏蔽
这里涉及到三种情况:

  1. 如果在[[Prototype]]链上层存在名为foo的普通数据访问属性并且没有被标记为只读(writable: false),那就会直接在myObj中添加一个名为foo的新属性,它是屏蔽属性。(PS:原型链上有同名属性,但是可以修改的情况下)
    示例代码:
var obj = {
  foo: 1
}
var myObj = Object.create(obj)
myObj.foo // 1
myObj.foo = 2
obj.foo // 1
  1. 如果在[[Prototype]]链上层存在foo,但是它被标记为只读(writable: false),那么无法修改已有属性或者在myObj上创建屏蔽属性。如果运行在严格模式下,代码会抛出错误。否则,这条赋值语句会被忽略。总之,不会发生屏蔽。
    示例代码:
var obj = {}
Object.defineProperty(obj, 'foo', {
  value: 1,
  writable: false,
  configurable: true,
  enumerable: true
})
var myObj = Object.create(obj)
myObj.foo // 1
myObj.foo = 2
myObj.foo // 1  这里还是1!
  1. 如果在[[Prototype]]链上层存在foo并且它是一个setter,那就一定会调用这个setterfoo不会被添加到myObj,也不会重新定义这个setter
    代码示例:
var obj = {}
Object.defineProperty(obj, 'foo', {
  configurable : true,
  enumerable : true,
  set: function() {
	this.value = 1
  },
  get: function () {
    return this.value
  }
})
var myObj = Object.create(obj)
myObj.foo = 2
myObj.foo // 1

如果希望在第二、三种情况下也能对myObj设置foo属性,那么请使用Object.defineProperty(),避免使用=

隐式屏蔽
摘抄自书上代码:

var anotherObject = {
  a: 2
}
var myObject = Object.create(anotherObject);
anotherObject.a // 2
myObject.a //2
anotherObject.hasOwnProperty('a') // true
myObject.hasOwnProperty('a') // false
myObject.a++ // 隐式屏蔽
anotherObject.a // 2
myObject.a // 3
myObject.hasOwnProperty('a') // true

上述代码关键点在myObject.a++这一步!myObject.a++相当于myObject.a = myObject.a + 1!!!myObject的原型对象anotherObject 上的a属性不是只读也没有固定死setter,所以这里相当于对myObject增加了一个a属性!!!

2.constructor属性

实例本身并没有constructor属性,constructor属性存在于它的原型对象上!!!
示例代码:

function Foo() {}
var f = new Foo()
f.constructor === Foo // true
Foo.prototype.constructor === Foo // true

之前学习原型链时知道了上述代码的关系,但是,其实这里我不是很懂。直到看了书我才知道,上述代码中f对象其实本身并没有constructor属性,它的原型对象Foo.prototype上才有,f.constructor本质上是去原型链上找到constructor属性即Foo.prototype.constructor!!!

这就是为什么当我们使用=操作符去进行原型继承时,需要对constructor重新赋值。
代码示例:

function A() {}
A.prototype.constructor // A
A.constructor // Function
function B() {}
B.constructor // Function
B.prototype.constructor // B
B.prototype // Function 这里是函数
B.prototype = new A() // A {} 这里是对象
B.prototype.constructor // A
B.constructor // Function
var c = new B() // B {}
c.__proto__ // A {} 这里就有问题了,c明明是由B构造的,却直接跳过了B找到了A
c.constructor === B.prototype.constructor // A 这里也有问题,不符合规范了
B.prototype.constructor = B // 强行赋值,改变指向

按照规范来说,B.prototype.constructor应该指向B自身,但是上述代码如果不手动改变指向,则会造成constructor属性指向错误!

3.面向委托的设计**

个人理解,面向委托的设计模式需要避免使用prototype以及constructor属性,主要通过this关键字来把控,配合Object.create()来实现。
典型的原型风格 与 面向委托两种不同设计模式实现对比:
类:

function Foo(who) {
  this.me = who
}
Foo.prototype.identify = function () {
  return "I am " + this.me
}
function Bar(who) {
  Foo.call(this, who)
}
Bar.prototype = Object.create(Foo.prototype)
Bar.prototype.speak = function () {
  alert("Hello, " + this.identify() + ".")
}
var b1 = new Bar("b1")
var b2 = new Bar("b2")
b1.speak()
b2.speak()

面向委托:

var Foo = {
  init: function (who) {
    this.me = who
  },
  identify: function () {
    return "I am " + this.me
  }
}
var Bar = Object.create(Foo)
Bar.speak = function () {
  alert("Hello, " + this.identify() + ".")
}
var b1 = Object.create(Bar)
b1.init("b1")
var b2 = Object.create(Bar)
b2.init("b2")
b1.speak()
b2.speak()

面向委托的写法看起来更简洁也更容易理解。而且,b1.constructor === Object; Foo.constructor === Object; Bar.constructor === Object!避免了原型继承模式下的constructor指向混乱的问题。

总结

面向委托的**是重点,以前从来都没接触过。但是需要不断实践才能真正深入了解。
接下来时间里继续看《你不知道的javascript》

重学js —— ECMAScript Specification Types

ECMAScript Specification Types

本篇主要为了方便阅读规范

规范类型对应于在算法中用于描述ECMAScript语言结构和ECMAScript语言类型语义的元值。包含以下类型:

规范类型值可用于描述ECMAScript表达式求值的中间结果,但此类值不能存储为对象的属性或ECMAScript语言变量的值。

List和Record规范类型

List类型用于解释那些使用 new 表达式、函数调用的 参数列 以及其它需要一个简单的有序值列表的算法。列表类型的值是包含单个值的列表元素的简单 有序序列。序列长度不固定。列表的元素可以使用从0开始索引随机访问。为了便于表示,可以使用 类似数组 的语法来访问列表元素。(应该都知道函数的arguments是个类数组对象吧,在规范类型里就是List类型,语言类型里是Object)

为了阅读规范需要,类似« 1, 2 »这种形式代表有两个元素的List值且每个值都会被初始化为特定的值。« »代表空的List。

Record类型用于描述本规范算法中的数据聚合。Record类型值由一个或多个命名字段组成。每个字段值要么是ECMAScript值,要么是由与Record类型关联的名称表示的抽象值。字段名总是用双括号括起来,例如[[Value]]

 为了便于在本规范中进行注释,可以使用类似于对象字面量的语法来表示记录值。例如, { [[Field1]]: 42, [[Field2]]: false, [[Field3]]: empty }定义了一个具有三个字段的Record类型值,并且每个字段有个特定值。Record类型中字段名顺序并不重要。没有明确列出来的字段视为不存在。
. 可能被用来表示指向一个Record值的某个字段。例如 R 是上一段中显示的record,R.[[Field2]]表示 R的[[Field2]]字段的缩写
应用案例:属性描述符:{ [[Value]]: 42, [[Writable]]: false, [[Configurable]]: true }

PS:在控制台打印一个{},点开__proto__属性,不出意外的话,会发现所有属性中都有一个[[Scopes]]属性。 当然,它依然是Object类型,不过却用了双括号来表示。

Set和Relation规范类型

Set类型用来解释内存模型使用的 无序 元素的集合。每个元素不会出现第二次。集合可以是并集、交集或相减的。(PS:注意与Set对象区分)

Relation类型用于解释Sets上的约束。Relation类型的值是其值域中的有序值对集。

例如,事件关联是一组有序的事件对。对于Relation R以及在它的值域中的两个值a和b:a R b是R的成员中有序对(a, b)缩写。
当一个Relation是满足这些条件的最小Relation时,它是关于某些条件的最小Relation。

Completion Record 规范类型

Completion类型其实是一种Record类型,用于解释值和控制流的运行时传播,例如执行非本地控制传输的语句(break、continue、return和throw)的行为。

字段名 说明
[[Type]] normal, break, continue, return, throw中的一个 发生过的completion
[[Value]] 任何ECMAScript语言值或空 产生的值 浏览器打印的数据基本上就是这个,chrome会把empty打印成 undefined。来自winter的《重学前端 —— try里面放return,finally还会执行吗?》
[[Target]] 任何ECMAScript string或空 定向控制转移的目标标签
// 一开始翻译时不大明白这些,继续看 winter的《重学前端 —— try里面放return,finally还会执行吗?》一章才算理解
// 看看下面代码
target:var test; // 不报错
// 这里的 target: 就对应 Completion Record 中的 `[[Target]]`

Completion Record 中的 [[Value]]

包含以下内容:

abrupt completion

所有 [[Type]] 不是 normalcompletion 都属于 abrupt completion

Reference规范类型

注意:Reference类型用于解释一些操作的行为,例如deletetypeof,赋值运算符,super关键字以及其它的语言特征。例如:赋值的左操作数应产生一个引用。

一个引用可能是一个已解析的名称或属性绑定。一个Reference包含三部分:基础值部分、引用的名称和严格引用标志的布尔值。基础值可能是undefinedObjectBooleanStringSymbolNumberEnvironment Record。基础值如果是undefined,那么无法将引用解析为绑定(其实就是对象某个属性值为undefined,该属性不绑定任何引用)。引用名是一个string或者symbol值。

一个Super Reference表示使用super关键字名称绑定的引用。Super Reference有额外的thisValue,并且它的基础值不可能是Environment Record。

Property Descriptor规范类型

Property Descriptor类型用来解释对象属性的属性的操作和具体化。(可以结合getOwnPropertyDescriptor来看)。PropertyDescriptor类型值是 Record类型。

属性描述符值可以根据某些字段的存在或使用,进一步被分类为 数据属性描述符访问器属性描述符 以及 通用属性描述符

  • 数据属性描述符: 包含名为 [[Value]]或[[Writable]] 的任何字段的描述符。
  • 访问器属性描述符:包含名为 [[Get]]或[[Set]] 的任何字段的描述符。
  • 通用属性描述符:一个属性描述符值,既不是数据属性描述符,也不是访问器属性描述符

任何属性描述符可能会有 [[Enumerable]] 和 [[Configurable]] 字段。属性描述符不能同时是数据描述符和访问器描述符,两者互斥!

Data Blocks

Data Blocks规范类型用于描述字节大小(8位)数字值的不同且可变的序列。一个Data Blocks值由固定数量的字节构成,且每个字节初始值为0。

在规范中,类数组的语法可用于访问Data Block值的各个字节。该表示法将Data Block值表示为从0开始的整数索引字节序列。

可以同时从多个代理引用的驻留在内存中的数据块称为 共享数据块(Shared Data Block)。共享数据块的标识(用于相等性测试共享数据块值)是无地址的:它不依赖于任何过程中块映射到的虚拟地址,而是依赖于该块所代表的内存中的位置集。仅当两个数据块所包含的位置集合相等时,它们才相等。 否则,它们不相等,并且它们包含的位置集的交集为空。

共享数据块的语义由内存模型使用共享数据块事件定义。下面的抽象操作引入共享数据块事件,并充当内存模型的求值语义和事件语义之间的接口。这些事件形成一个候选执行,内存模型在其上充当筛选器。有关完整的语义,请参考内存模型

共享数据块事件由内存模型中定义的记录建模。

重学js——ES规范定义的任务和任务队列(Jobs and Job Queues)

Jobs and Job Queues

Job 是抽象操作,当 当前没有其他ECMAScript计算正在进行时,它将启动ECMAScript计算。可以将 Job 抽象操作定义为接受任意一组任务参数。

只有当 没有运行时执行上下文 并且 执行上下文栈为空 时任务才会被启动执行。PendingJob 是对将来执行任务的请求。PendingJob 是一个内置的 Record,字段见下表:

字段名 意义
[[Job]] 任务抽象操作名 启动执行此 PendingJob 时执行的抽象操作。
[[Arguments]] List类型值 表示当 PendingJob 激活时,会被传递给[[Job]]的参数列。
[[Realm]] 领域记录 PendingJob 启动时的初始执行上下文的领域记录。
[[ScriptOrModule]] Script Record or Module Record PendingJob 启动时的初始执行上下文的脚本或模块。
[[HostDefined]] 可以是任何值,默认值是 undefined 保留给需要将其他信息与 PendingJob 关联的主机环境使用的字段。

一旦某个任务启动执行,该任务将会一直执行直到完成。在当前运行的任务完成前,其它任何任务都不会启动。 但是,当前正在运行的任务或外部事件可能会导致其他 PendingJob 排队,这些任务可能在当前正在运行的任务完成后的某个时间启动。

任务队列是PendingJob记录的先进先出队列。每个任务队列都有名字并且完整的可用任务队列集由ECMAScript实现定义。每个ECMAScript实现至少有下表中列出的 两种 任务队列:

名称 意义
ScriptJobs 验证和评估ECMAScript脚本和模块源文本的任务队列。见规范1015章节。
PromiseJobs 响应 Promise 解决的任务队列(见Promise Objects)

通过在任务队列上使包含任务抽象操作名称和任何必要参数值的 PendingJob 记录入队,来请求将来执行任务。当没有运行时执行上下文和执行上下文栈为空时,ECMAScript的实现从任务队列中移除第一个 PendingJob 并且使用 PendingJob 包含的信息创建执行上下文并同时始执行关联的Job抽象操作。

来自单个任务队列的 PendingJob 记录始终以先进先出顺序启动。规范没有定义服务多个任务队列的顺序。 ECMAScript实现可以将任务队列的 PendingJob 记录的先进先出求值与一个或多个其他任务队列的 PendingJob 记录的求值交织在一起。一个实现必须定义当没有运行时执行上下文并且所有任务队列为空时会发生什么。

通常,ECMAScript实现将使用至少一个 PendingJob 预先初始化其任务队列,并且其中一个任务将是第一个要执行的任务。

如果当前任务完成并且所有任务队列为空,则实现可以选择释放所有资源并终止。或者,它可以选择等待某些特定于实现的代理或机制来排队新的 PendingJob 请求。

EnqueueJob ( queueName, job, arguments )

创建和管理任务以及任务队列的步骤:

  1. Assert(断言)Type(queueName)是 String 类型并且其值是被实现认可的任务队列的名字。
  2. Assert(断言)job是任务的名字。
  3. Assert(断言)arguments是一个具有与任务所需参数数量相同的元素数量的列表。
  4. 让过程变量callerContext保存运行时执行上下文。
  5. 让过程变量callerRealm成为callerContext的领域。
  6. 让过程变量callerScriptOrModule成为callerContextScriptOrModule
  7. 定义过程变量pedding,初始值为PendingJob { [[Job]]: job, [[Arguments]]: arguments, [[Realm]]: callerRealm, [[ScriptOrModule]]: callerScriptOrModule, [[HostDefined]]: undefined }.
  8. 执行任何实现或主机环境定义的pedding处理。这可能包括修改[[HostDefined]]字段或pedding的任何其他字段。
  9. 在由queueName命名的任务队列的后面添加pedding
  10. 返回NormalCompletion(empty).

RunJobs ()

任务队列的执行步骤:

  1. 执行 InitializeHostDefinedRealm()
  2. 以依赖于实现的方式,获取零个或多个ECMAScript脚本和/或ECMAScript模块的ECMAScript源文本(请参见第10节Source Code)和任何相关的主机定义值。对于每个这样的sourceTexthostDefined
    • 如果sourceText是脚本源代码,那么
      • 执行EnqueueJob("ScriptJobs", ScriptEvaluationJob, « sourceText, hostDefined »).
    • 否则,
      • 断言:sourceText是模块源代码
      • 执行EnqueueJob("ScriptJobs", TopLevelModuleEvaluationJob, « sourceText, hostDefined »)
  3. 重复执行下面操作,
    • 挂起运行时执行上下文并且把它从执行上下文栈中移除。
    • 断言:执行上下文栈现在是空的。
    • 定义过程变量nextQueue,让它成为以实现定义的方式选择的非空任务队列。如果所有的任务队列为空,那么结果由实现定义。
    • 定义过程变量nextPending,令其为nextQueue前面的 PendingJob 记录。从nextQueue移除该记录。
    • 定义过程变量newContext作为新的执行上下文
    • 设置newContextFunction字段值为 null
    • 设置newContextRealm字段值为nextQueue.[[Realm]]
    • 设置newContextScriptOrModule字段值为nextPending.[[ScriptOrModule]]
    • newContext推入执行上下文栈中;这时newContext位于栈顶,成为当前的运行时执行上下文
    • 使用nextPending执行任何实现或主机环境定义的任务初始化。
    • 定义过程变量result,使其成为 执行由nextPending.[[Job]]定义并使用了nextPending.[[Arguments]]作为参数的 抽象操作的 结果。
    • 如果result是一个abrupt completion(非normal类型的completion),执行HostReportErrors(« result.[[Value]] »)

注意

经典面试题:

console.log('start')
setTimeout(() => console.log('setTimeout'), 0)
new Promise(res => {
  console.log('new Promise')
  res()
}).then(val => console.log('Promise then'))
console.log('end')

// 合理的打印结果为(有的浏览器实现有问题)
start
new Promise
end
Promise then
setTimeout

这条题涉及到 宏任务微任务 概念,可惜本文全篇都没有提到这两个术语,而且,整个规范都没有提到!!!
我就懵逼了,难不成不是规范,那么为何有那么多人统一叫这两个专业术语呢???
后来看知乎才知道,这两个术语出自 HTML Event Loops规范!!!

拓展阅读

  1. 宏任务和微任务
  2. Using microtasks in JavaScript with queueMicrotask()
  3. ECMAScript 的 Job Queues 和 Event loop 有什么关系?(看flyingsoul的回答)
  4. Event Loops
  5. MDN —— 并发模型与事件循环

重学js —— 比较

比较

先看代码:

1 > 0 // true
NaN > 0 // false
NaN === NaN // false 
Infinity === Infinity // true
0n === 0 // false
0n == 0 // true
true > false // true
'A' > 'B' // false
'A' > 'a' // false
'a' > 'A' // true
null == undefined // true
null === undefined // false
{} < {} // false
{} > {} // false
{} == {} //false
{} === {} //fasle
Symbol() > 0 // 报错
Symbol() == Symbol() // false
Symbol() === Symbol() // false
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
new String('a') == new String('a') // false

上面的代码,我能说出所有的正确打印结果吗?如果能的话,我能说出所有的比较原理吗?

对不起,我都做不到。比如对象之间比较以及 SymbolNumber 的比较,我不敢说出结果,因为我没手动测试过,更对其原理一无所知,所以我遇到这些完全没底气!

其他的方法可以不看,主要是以下的与比较有直接关联的方法:

IsStringPrefix ( p, q )

判断字符串 p 是不是字符串 q 的前缀。

  • pq 都必须是字符串
  • 如果 q 可以是 p 和其他字符串 r 的串联,返回 true,否则返回 false
  • 注意:任何字符串都是其自身的前缀,r 可能是空字符串

SameValue ( x, y )

  • 判断类型是不是一样,不是返回 fasle
  • 判断 x 是不是 NumberBigInt,然后对 xy 执行 sameValue (BigInt::sameValue ( x, y )Number::sameValue ( x, y ))操作比较
  • 不符合以上条件,直接返回调用 SameValueNonNumeric(x, y) 后的值

Object.is 采用该算法,与严格相等不同的是对NaN以及+0, -0的判断。它认为NaN是相等的,但同时认为+0和-0不等。严格相等使用equal算法!!!

SameValueZero ( x, y )

其实本质和 SameValue 区别不大,只是它只比较 +0-0

SameValueNonNumeric ( x, y )

最终会返回 truefalse

  • 要求 xy 不能是NumberBigInt
  • 要求 xy 类型一致
  • 如果 xundefined,返回 true
  • 如果 xnull,返回 true
  • 如果 xString
    • 如果 xy 是完全相同的代码单元序列(在相应的索引处具有相同的长度和相同的代码单位),返回 true,否则返回 false
  • 如果 xBoolean
    • 如果 xy 都是 true 或者都是 false,则返回 true,否则返回 false
  • 如果 xSymbol
    • 如果 xy 是相同的 Symbol ,返回 true,否则返回 false
  • 如果 xy 是相同的对象 ,返回 true,否则返回 false

抽象关系比较

比较 x < y(其中 xy 是值)会产生 truefalseundefined(这表明至少一个操作数是 NaN)。除x和y外,该算法还使用名为 LeftFirst 的布尔标志作为参数。该标志用于控制对 xy 执行具有潜在可见副作用的操作的顺序。LeftFirst 默认为 true

  • 如果 LeftFirsttrue
    • 定义过程变量 px,执行ToPrimitive(x, hint Number)(如果是 Object,默认先执行 valueOf(),不满足再执行 toString()
    • 定义过程变量 py,执行ToPrimitive(y, hint Number)(如果是 Object,默认先执行 valueOf(),不满足再执行 toString()
  • 否则,
    • 注意:需要颠倒顺序以保持从左到右运算
    • 定义过程变量 py,执行ToPrimitive(y, hint Number)(如果是 Object,默认先执行 valueOf(),不满足再执行 toString()
    • 定义过程变量 px,执行ToPrimitive(x, hint Number)(如果是 Object,默认先执行 valueOf(),不满足再执行 toString()
  • 如果 pxpy 都是 String
    • 执行IsStringPrefix(py, px),如果结果为 true,则返回 faslepx包含py
    • 执行IsStringPrefix(px, py),如果结果为 true,则返回 truepy包含px
    • 如果 pxpy 不具有 前缀(必须是前缀) 包含关系,则定义过程变量 k 为最小非负整数,以便 px 内索引 k 处的码点与 py 内索引 k 处的码点不同。(必须有一个 k,因为两个字符串都不是另一个的前缀)
    • 定义过程变量 m 为整数,该整数是 px 以内的索引 k 处的码点的数值
    • 定义过程变量 n 为整数,该整数是 py 以内的索引 k 处的码点的数值
    • 如果 m < n,返回 true,否则返回 false这里主要比较的是字符串在Unicode码表中的码点数值
  • 否则,
    • 如果 pxBigIntpyString
    • 如果 pyBigIntpxString
    • 定义过程变量 nx,执行ToNumeric(px) 将结果赋值给 nxpxpy 此时已经都是primitive(原始,这里应该都是 数值)值了,顺序不重要)
    • 定义过程变量 ny,执行ToNumeric(py) 将结果赋值给 ny
    • 如果 nxny 类型一致,执行对应类型的 lessThan(nx, ny)Number::lessThan (nx, py)BigInt::lessThan(nx, py))方法
    • 如果 nxBigInt 类型并且 nyNumber 类型,或者两者类型对调
    • 如果 nxnyNaN,返回 undefined
    • 如果 nx-Infinityny+Infinity,返回 true
    • 如果 nx+Infinityny-Infinity,返回 false
    • 如果 nx 的数学值比 ny 的数学值小,返回 true,否则返回 false

字符串比较,先比较是不是前缀包含。何为前缀包含?例如abca 可以认为是前缀包含,但是 abcb 则不是,因为它需要 a + b + cb 不是 abc 的前缀。

字符串的比较,如果两字符串不具有包含关系,则 从两个字符串的第一个下标开始,比较同一下标对应的字符在Unicode码表中的码点数值大小。一旦某个下标所对应的字符分出了大小,则比较结束

注意:NullUndefiend 以及 Boolean 类型比较会在执行ToNumeric(px)时转换成数值。

抽象相等 ==

日常使用的 ==

  • 如果 xy 的类型一致,则返回 x === y 的结果。(即类型一致,默认采用全等比较
  • 如果 xnull 同时 yundefined,返回 true
  • 如果 xundefined 同时 ynull,返回 true
  • 如果 xNumber 类型,yString 类型,将 y 转为 Number 然后继续比较
  • 如果 xString 类型,yNumber 类型,将 x 转为 Number 然后继续比较
  • 如果 xBigInt 类型,yString 类型,
    • 定义过程变量 n,执行StringToBigInt(y)将结果赋值给 n
    • 如果 nNaN,返回 false
    • 否则,返回 x == n 的结果
  • 如果 xString 类型,yBigInt 类型,返回 y == x 的结果(其实就是位置对调然后重复上一步)
  • 如果 xBoolean 类型,将 x 转为 Number 类型,然后比较转为 Number 类型后的值与 y 比较结果
  • 如果 yBoolean 类型,将 y 转为 Number 类型,然后比较转为 Number 类型后的值与 x 比较结果
  • 如果 xStringNumberBigIntSymbol 类型,yObject 类型,先将 y 转为原始类型,然后再比较(执行 ToPrimitive(y)
  • 如果 xObject 类型,yStringNumberBigIntSymbol 类型,先将 x 转为原始类型,然后再比较(执行 ToPrimitive(x)
  • 如果 xBigInt 类型,yNumber 类型,或者 xNumber 类型,yBigInt 类型
    • 如果 xy 有一个是 NaN+Infinity-Infinity,返回 false(事实上,BigInt 不支持这三个值)
    • 如果 x 的数学值和 y 的数学值相等,返回 true,否则返回 false
  • 以上都不满足,返回 false

注意:StringNumberBooleanNumber 比较,最终都会转为 Number 类型进行比较!

严格相等 ===

就是日常使用的 === 全等。

注意,这里使用的是equal算法,而Object.is使用的是sameValue算法

回归开头代码

首先是字符串的比较,我知道了它会先判断是不是前缀,否则直接开始比较相同下标对应的字符在Unicode中的码点大小:

'A' > 'B' // false
'A' > 'a' // false
'a' > 'A' // true

接着是全等,这个其实不难,既要判断类型又要判断值是不是都一样:

0n === 0 // false
null === undefined // false
NaN === NaN // false 
Infinity === Infinity // true
{} === {} //fasle
Symbol() === Symbol() // false

容易困惑的在于NaNSymbol

  1. 首先 NaN 属于 Number 类型,适用于Number::equal ( x, y ),不论谁是 NaN,都返回 false!!!
  2. 其次是 Object,它最终比较的是地址内存值
  3. 还有 Symbol,有点 Object 的意思。

接下来是普通 ==,它只要求值相等不强调类型:

0n == 0 // true
null == undefined // true
{} == {} //false
Symbol() == Symbol() // false

最后,则是其它的大小比较了:

true > false // true
NaN > 0 // false
{} < {} // false
{} > {} // false
Symbol() > 0 // 报错
  1. Boolean 会被转为 Number 来比较
  2. NaN > 0 会执行Number::lessThan (nx, py),有NaN,根据规范应该返回undefined,不过浏览器把它实现成了 false!!!
  3. Symbol() > 0 会尝试把 Symbol 转为 Number,可是根据类型转换规则,会报类型错误
  4. {} < {}{} > {} 都会尝试转为原始值在进行比较,可惜,它们最终都会转为 "[object Object]",两个最终字符串相同,所以只能是 false!!!

额外补充Object.is

如果仔细看内容的话就知道为什么了。根本在于 Object.is 内部使用的是 ::sameValue ( x, y ) 算法,而严格相等使用的是 ::equal ( x, y ) 算法!

所以两者在 NaN 以及 +0-0 的比较上不同

重学js —— js数据类型:Object 基础介绍(内部方法和内置插槽等)

Object 基础介绍

这里不涉及具体某个api,因为实在太多了。

对象的字符串属性键名 整数索引 在 0 ~ 253 - 1间(源于string的长度限制)。

数组也是对象。但是数组的索引范围是 0 ~ 232 - 1。因为数组长度内部用了 ToUint32 转换的,相关算法在规范中多处都有提到,例如 Array 的 [[DefineOwnProperty]](P, Desc)

属性有两种访问方式:getset,分别对应取值和赋值。通过getset可以访问对象自身属性和继承属性。

JavaScript 中的对象分类

来源于规范中定义的对象以及winter《重学前端》——JavaScript对象:你知道全部的对象分类吗?。

  • host Objects(宿主对象):由 JavaScript 宿主环境提供的对象,它们的行为完全由宿主环境决定。
  • Built-in Objects(内置对象):由 JavaScript 语言提供的对象。
    • Ordinary objects(普通对象):最常见的对象形式,并具有默认的对象语义。(由{}语法、Object 构造器或者 class 关键字定义类创建的对象,它能够被原型继承。)
    • Intrinsic Objects(固有对象):由标准规定,随着 JavaScript 运行时创建而自动创建的对象实例。
    • Native Objects(原生对象):可以由用户通过 Array、RegExp 等内置构造器或者特殊语法创建的对象。PS:规范中并没有找到相应的分类,单纯的 Array 属于固有对象,但是其生成的数组对象被winter划分到这里。

PS:任何不是 Ordinary objects 的对象都是 exotic object (奇异/怪异对象)。

属性描述符

注意:js对象其实是key-value集合,每个key都有相应的属性描述符!根据属性描述符的不同可以分为 数据属性访问器属性

数据属性的属性描述符见下表:

属性名 值域 描述
[[Value]] 符合ECMAScript语言类型的值 通过属性的get访问获取的值。
[[Writable]] Boolean 如果为false,通过ECMAScript代码试图使用[[Set]]改变该属性[[Value]]特性不会成功
[[Enumerable]] Boolean 如果为true,该属性能被for-in枚举(见13.7.5)。否则,该属性不可枚举。
[[Configurable]] Boolean 如果为false,尝试删除该属性,将该属性转换为访问器属性或者改变它的特性([[Value]]以外的其他值,或将[[Writable]]更改为false)等操作都会失败。[[Value]]依然可以改。

js代码示例:

let obj = {};
Object.defineProperty(obj, 'a', {
  value: 'test',
  writable: true,
  enumerable: true,
  configurable: true
})

访问器属性的属性描述符见下表:

属性名 值域 描述
[[Get]] Object / Undefined 如果值为对象,那么必须是函数对象。使用空参数列表调用函数的[[Call]]内置方法(见),每次执行属性的get访问时检索属性值。
[[Set]] Object / Undefined 如果值为对象,那么必须是函数对象。每次执行属性的set访问时会使用包含 赋值作为其唯一参数的 参数列表调用函数的[[Call]]内置方法(见)。属性的[[Set]]内部方法的效果可能(但不是必需)对后续调用该属性的[[Get]]内部方法所返回的值产生影响。
[[Enumerable]] Boolean 如果为true,该属性能被for-in枚举(见13.7.5)。否则,该属性不可枚举。
[[Configurable]] Boolean 如果为false,尝试删除该属性,将该属性转换为数据属性或改变该属性的特性(描述符)都会失败。

js代码示例:

let val;
Object.defineProperty(obj, 'b', {
  get () {
    console.log('get...');
    return val;
  },
  set (newVal) {
    val = newVal;
    console.log('set...');
  },
  enumerable: true,
  configurable: true
})
obj.b = 1; // set...
obj.b; // get...  // 1

对象的内部方法和内部插槽

ECMAScript中对象的实际语义是通过称为 内部方法 的算法指定的。ECMAScript引擎中的每个对象都与一组内部方法相关联,这些内部方法定义了其运行时行为。这些内部方法不是ECMAScript语言的一部分。由规范定义,仅出于说明目的。

内部插槽对应于与对象关联并由各种ECMAScript规范算法使用的 内部状态。内部插槽不是对象属性,也不会继承。根据特定的内部插槽规范,这种状态可以由任何 ECMAScript语言类型的值特定的ECMAScript规范类型的值 组成。除非另有明确说明,否则内部插槽将作为创建对象过程的一部分进行分配,并且可能不会动态添加到对象中。除非另有说明,否则内部插槽的初始值为 undefined。规范中的各种算法都会创建具有内部插槽的对象。但是,ECMAScript语言没有提供将内部插槽与对象关联的直接方法。

在规范中,内部方法和内部插槽使用 [[内部方法名 或者 内部插槽名]] 表示(符合Record类型)。下表总结了规范使用的 基本内部方法,这些方法适用于由ECMAScript代码创建或操作的所有对象。每个对象都必须具有用于所有基本内部方法的算法。但是,对于这些方法,所有对象不一定都使用相同的算法。

内部方法 签名 描述
[[GetPrototypeOf]] ( ) → Object / Null 确定为该对象提供继承属性的对象。null表示没有继承属性。Object.getPrototypeOf()
[[SetPrototypeOf]] (Object / Null) → Boolean 将此对象与提供继承属性的另一个对象相关联。传null表示没有继承属性。返回true表示操作成功,返回false表示操作失败。(Object.setPrototypeOf()
[[IsExtensible]] ( ) → Boolean 确定是否允许向该对象添加其他属性。(对应Object.isExtensible()
[[PreventExtensions]] ( ) → Boolean 控制新属性是否能被加入对象内。返回true表示操作成功,返回false表示操作失败。(PS:如果为false,即使[[IsExtensible]]true,添加新属性操作也会失败。Object.preventExtensions()
[[GetOwnProperty]] (propertyKey) → Undefined / Property Descriptor 返回此对象自身属性的属性描述符,如果没有对应的属性,返回undefinedObject.getOwnPropertyDescriptor()Object.getOwnPropertyDescriptors()
[[DefineOwnProperty]] (propertyKey, PropertyDescriptor) → Boolean 创建或更改自己的属性,该属性对应传入的propertyKey,属性描述符对应传入的PropertyDescriptor。如果操作成功返回true,否则返回false。(对应Object.defineProperty()Object.defineProperties()
[[HasProperty]] (propertyKey) → Boolean 返回一个布尔值,该值指示此对象是否其自身已具有或继承的键为传入的propertyKey的属性。(对应Object.prototype.hasOwnProperty()
[[Get]] (propertyKey, Receiver) → any 返回对象中属性名为参数propertyKey的值。如果必须执行ECMAScript代码来找到属性值,参数Receiver会被当作this来用。
[[Set]] (propertyKey, value, Receiver) → Boolean 将参数value设置为对象中属性名为参数propertyKey的值。参数Receiver会被当作this来用。如果操作成功返回true,否则返回false
[[Delete]] (propertyKey) → Boolean 删除对象中属性名为参数propertyKey的属性。删除成功返回true,否则返回false。(类似js中的delete关键字)
[[OwnPropertyKeys]] ( ) → List of propertyKey 返回一个列表,其元素都是对象自己的所有属性键。(Object.getOwnPropertyNames()Object.getOwnPropertySymbols()

下表总结了函数对象所支持的其他基本内部方法:

内部方法 签名 描述
[[Call]] (any, a List of any) → any 执行与此对象关联的代码。通过函数调用表达式调用。第一个参数any表示thisa List of any表示传入的参数列表。实现此内部方法的对象是可调用的。
[[Construct]] (a List of any, Object) → Object 创建一个对象。通过newsuper操作调用。a List of any表示包含运算符参数的列表。第二个参数Object表示new操作的初始应用对象。实现该内部方法的对象称为constructors。函数对象不一定要有constructor并且非构造函数对象没有[[Construct]]内部方法。

基本内部方法的不变量

定义:

  • 内部方法被调用的对象就是其目标target
  • 如果目标对象的[[IsExtensible]]内部方法返回false或者其[[PreventExtensions]]返回true,则该对象是不可扩展的。
  • 不存在的属性是指 不可扩展的对象自身没有该属性。
  • SameValue的所有引用均根据SameValue算法的定义。

返回值:

任何内部方法的返回值必须是具有以下任一内容的完成记录

  • [[Type]] = normal, [[Target]] = empty, 并且 [[Value]] = normal类型返回的值, 或
  • [[Type]] = throw, [[Target]] = empty, 并且 [[Value]] = 任何ECMAScript语言类型值。

注意:内部方法不会返回[[Type]]为continue, break, 或 return的completion。

2020-07-27 补充

var a = {n: 1};
var b = a; // 指向 {n: 1} 这个对象
a.x = a = {n: 2}; // 此时 a 指向 {n: 2} 这个对象
console.log(a.x); // undefined
console.log(b); // {n: 1, x: {n: 2}}
console.log(b.x); // {n: 2}

这里问题关键在于 a.x = a = {n: 2} 这一段,其实 a.x 此时应该是 {n: 1} 这个对象添加了个新属性 xa = {n: 2} 则是将 a 变量指向新对象!然后 {n: 1} 这个对象的 x 属性指向 {n: 2} 这个对象。

重学js —— Array.isArray——严格判定JavaScript对象是否为数组

严格判定JavaScript对象是否为数组

先看MDN:Array.isArray()

能准确区分数组和类数组对象!

接下来是翻译:

JavaScript中的类型校验问题

JavaScript的 typeof 操作符有着令人困惑的表现行为:typeof null === "object",而 typeof null !== "null"。这个错误会让人震惊,但我们基本上已经习惯了。可能更重要的是,有一个故障安全的解决方法:简单直接的用 === (全等)比较,例如 v === null 来进行判断。

确定值是否为数组

typeof null === "object" 可能是最常见的类型校验错误,不过还存在其它的类型校验错误。一个不太普遍但同样令人困惑的问题是 如何确定对象是否为数组。你可能会有个简单的解决方案:

if (o instanceof Array) {
  // 太糟糕了
}

在某些情况下,上面代码运行完美。但是如果存在 多个全局变量 就会出现问题。

ECMAScript规范描述了当执行一串代码时调用的环境和机制。语言构造的语法和基本语义当然很重要,但是如果没有ECMAScript中的内置方法和对象编码,就不能很好工作。这些方法和对象被全局对象使用,这里就是奇怪事情的发生地。ECMAScript 3 环境隐式假定存在单个全局变量(或者,也许是每个独立部分都有自己的环境,彼此之间没有相互作用)并且没有解决多个全局变量的想法。

但是,多个全局变量是浏览器的基础;每个 window 对象是该页面包含或引用的脚本的全局对象。那么在不同 window 中的数组呢?

当两个页面都增加 Array.prototype 时,将两个 window 的数组作为同一 Array 构造函数的实例,共享相同 Array.prototype 的 变化风险 非常大(更不用说当一个页面为恶意页面时的安全问题!),因此每个窗口中的 ArrayArray.prototype 必须不同。所以,仅当 o 是由该页面原始 Array 构造器创建的数组(或者使用该页面数组字面量创建),o instanceof Array 才会运行正确。

是否还有其他可以确定值是数组的方法?

  • o.constructor === Array 是一种方法,但和 instanceof 有相同的问题。
  • 另一个选择依赖于 “鸭子类型”**,当值看起来像数组,那么它就是数组。
  • 沿着 constructor 检查,您可以检查其他数组方法(例如 pushconcat),或者检查 length 属性,但是非数组对象也可以有这些同名属性。
  • 使用Object.prototype.toString.call(o) === "[object Array]",但是依赖于 Object.prototype.toStringFunction.prototype.call 未被改变。

来到 Array.isArray

由于这些原因,ES5定义了 Array.isArray 方法来完全解决该问题。

Array.isArray(Array.prototype); // true

function test(fun, expect) { 
  if (fun() !== expect) alert("FAIL: " + fun); 
}

test(function() { 
  return Array.isArray([]); 
}, true);

test(function() { 
  return Array.isArray(new Array); 
}, true);

test(function() { 
  return Array.isArray(); 
}, false);

test(function() { 
  return Array.isArray({ constructor: Array }); 
}, false);

test(function() { 
  return Array.isArray(   
    { 
      push: Array.prototype.push, 
      concat: Array.prototype.concat 
    }
  ); 
}, false);

test(function() { 
  return Array.isArray(17); 
}, false);

Object.prototype.toString = function() { return "[object Array]"; };

test(function() { 
  return Array.isArray({}); 
}, false);

test(function() { 
  return Array.isArray({ __proto__: Array.prototype }); 
}, false);

test(function() { 
  return Array.isArray({ length: 0 }); 
}, false);

var w = window.open("about:blank");
w.onload = function()
{
  test(function() { 
    return Array.isArray(arguments); 
  }, false);
  test(function() { 
    return Array.isArray(new w.Array); 
  }, true);
};

规范

  1. Array.isArray( arg )
  2. IsArray(arg)

重学js —— js数据类型:String

String 类型

String 类型是所有0个或多个16位无符号 整数 值组成且最大长度为 253 - 1 的有序序列集合。

String类型通常用于表示正在运行的ECMAScript程序中的文本数据,字符串中的每个元素被当作 UTF-16 码元值处理。每个元素被认为在序列中占据一个位置。这些位置用非负整数索引。第一个元素下标为 0((即下标从0开始)),下一个元素下标为 1,依此类推。字符串长度是字符串中的元素数量。空字符串长度为0且不包含元素。

解释字符串值的操作将每个元素视为单个UTF-16码元。但是,ECMAScript不限制这些码元的值或它们之间的关系,因此,将字符串内容进一步解释为以 UTF-16 编码的 Unicode 码点序列的操作必须考虑格式错误的子序列。这样的操作会对数值范围在 0xD8000xDBFF (由Unicode标准定义为 主要代理,或更正式地定义为高代理码元)范围内的每个码元都进行特殊处理,且每个数值在 0xDC000xDFFF (定义为 尾代理,或更正式的定义为 低代理码元)范围内的码元使用以下规则:

  • 既不是 主要代理 又不是 尾代理 的码元被解释为具有相同值的码点
  • 两个码元的序列,如果第一个码元 c1主要代理 且第二个码元 c2尾代理,会组成一个 代理对,被解释为码点,其值为 (c1 - 0xD800) × 0x400 + (c2 - 0xDC00) + 0x10000 (见 UTF16DecodeSurrogatePair)。
  • 属于 主要代理尾代理 但又不属于 代理对 的码元,被解释为具有相同值的码点

String.prototype.normalize 能显式规范字符串值。String.prototype.localeCompare 在内部对String值进行规范化,但是在其运算时没有其他操作隐式地规范字符串。

注意:这种设计的基本原理是使Strings的实现尽可能简单和高性能。如果ECMAScript源文本为规范化格式C,保证字符串文字也可以规范化,只要它们不包含任何Unicode转义序列。

在该规范中,"the string-concatenation of A, B, ..."(其中每个参数是一个字符串值,一个码元或码元序列)表示字符串值是每个参数的码元 顺序的串联形成码元序列。

String 值

字符串值是基本值,它是零个或多个16位无符号 整数 值的有限有序序列。

注意:序列上的每个 整数 值通常表示UTF-16文本的一个单个16位单元。但是,ECMAScript对值没有任何限制或要求,只不过它们必须是16位无符号整数。

String 字面量

注意:字符串字面量是以单引号或双引号括起来的0个或多个 Unicode 码点。Unicode 码点也可以由转义序列表示。除结尾引号码点,U+005C(反斜杠)U+000D(回车符) 以及 U+000A(换行符) 外,所有码点都能以字符串形式显示。任何码点都能以转义序列的形式出现。字符串字面量的计算结果为ECMAScript字符串值。生成这些字符串值时,Unicode 码点按照 UTF16Encoding 中的定义进行UTF-16编码。属于 基本多文种平面 的码点被编码为字符串的单个代码单元。除此之外所有其它码点会被编码为字符串的两个代码单元。

字符串单字符转义序列
转义序列 代码单元值 Unicode字符名 符合
\b 0x0008 BACKSPACE (退格) <BS>
\t 0x0009 CHARACTER TABULATION (水平制表符) <HT>
\n 0x000A LINE FEED (LF 换行) <LF>
\v 0x000B LINE TABULATION (垂直制表符) <VT>
\f 0x000C FORM FEED (FF 换页) <FF>
\r 0x000D CARRIAGE RETURN (CR 回车) <CR>
\" 0x0022 QUOTATION MARK (双引号) "
\' 0x0027 APOSTROPHE (单引号) '
\\ 0x005C REVERSE SOLIDUS (反斜杠) \

疑问

"整数值(integer)"是什么意思?

规范中有说明:在本说明书中使用 integer 时,除非另有说明,否则是指 Number value 且它的数学值(mathematical value)在 整数集 中;在本说明书中使用数学整数(mathematical integer)时,是指在 整数集 中的数学值(mathematical value)。方便速记,integert 可以用来指代Number value或mathematical value中任何一个,用 t 来代替。

16位无符号是什么意思?

这里16位应该指的是16位二进制数;可以近似认为是 UTF-16编码
16位无符号整数代表不区分正负,那么其能表示Math.pow(2, 16)个数,即65536个,从0开始即:

// 二进制
00000000 00000000 ~ 11111111 1111111
// 对标十进制
0 ~ 65535

最大长度为何是Math.pow(2, 53) -1,依据是什么?

其实这个问题答案在于问题1 integerinteger 不管指代Number value还是mathematical value,都有个要求:mathematical value在整数集上。

什么是整数集?
所有整数组成的集合

这就涉及到ECMAScript的Number类型了。而Number类型可表示的最大安全值是9007199254740991Math.pow(2, 53) - 1。所以整数集中最大长度就是Math.pow(2, 53) - 1

ECMAScript要求string value中的每个值都必须是16位无符号整数,可是正常来说string里面既可以是数字也可以是英语字母、中文以及其他语言甚至特殊字符,这些东西都满足16位无符号整数这个要求吗?或者问题可以改为string到底是Unicode还是ASCII编码?

这个问题其实我一开始完全不懂编码,我都不知道unicode和ASCII编码有什么关系?而且当时看规范很吃力,没办法就去知乎问了下,链接:https://www.zhihu.com/question/350713984

先看winter的回答,他说的很对,其实我对字符集这些东西根本就不懂,理解起来也乱七八糟。。。

注意:string没有值,它是字符串,由一个个字符组成,字符有码点,码点才是16位无符号整数。

要继续往下面理解,就必须先懂字符码点相关知识,所以可以先看下面的链接:
#47

结论是采用 Unicode字符集(正如规范中多次提到那样)。ecma402 comparestrings

vue父组件异步获取数据并props传给子组件,但是子组件没有及时更新的问题

问题来源

  记得第一次用vue开发项目时,曾经遇到过父组件调接口改变data里面的某个属性值并通过props传递给子组件,但是子组件并没有实时刷新的问题,当时试了下用watch监听,就解决了。当时初学,vue不怎么懂,更别说源码了,最近项目中又出现过一次这种情况,这就很奇怪了,我必须要解决这个问题。

示例:https://blog.csdn.net/d295968572/article/details/80810349 PS:这是网上其他人的问题,大致与我一样。

思考及查看源码

  根据我提出的问题,可以意识到,这种情况props传过来没有及时更新,但是watch监听却能获取到数据,那么watchprops之间有什么区别呢?官方文档也说父组件动态改变props值,子组件是能获取到的,但是我这里为何不行呢?

  根据上面问题,我先去查看关于props以及watch的源码,其实以前有阅读过watch源码,知道它最终会走defineReactive方法进行观察。但令我没想到的是,props最终也走这个方法。。。

  关于这两个初始化,可以去state.js里面找initPropsinitWatch方法。

  再回顾一下initWatch方法,它内部调用了createWatcher方法(PS:computed初始化也走这个方法了),盖凡凡最终返回vm.$watch(expOrFn, handler, options),而$watch就在下面stateMixin方法内,stateMixininstance/index.js 中执行。$watch里面执行了const watcher = new Watcher(vm, expOrFn, cb, options)这段代码,其实就是实例化了一个Watcher

  然后Watcher里面调用get方法,get方法中调用pushTargetpushTarget在dep.js里,对Dep.target赋值,接着继续看Watcher里面的get方法,这里直接value = this.getter.call(vm, vm),最后return value了。这里就很奇怪,这样看下来根本没有涉及到依赖监听机制啊,按道理不对啊。

  而且,我们只会对props以及在data里面初始化(或者通过this.$set设置)的属性进行watch,但是这里并没有判断被watch的属性是否存在this实例上,也没有判断被watch的属性是不是observe的,难不成我随便瞎写一个属性进行watch也行?

看示例:

如上图,即使被watch的属性并不存在,watch也能执行,只是undefined罢了!

这只是其中的一个小插曲,回到问题上来。

那么,如何实现依赖监听的呢?由于有之前阅读源码的基础,我记得最终watch也是要执行defineReactive的,他在哪里执行的呢?

答案是initDatainitProps里面,其中initData里面铁定调用observe进行观察,Observer中又会生成新的Dep实例,并通过walk方法执行defineReactive

defineReactive里面又生成新的Dep实例,到了这个方法大家没差别,说明props传递进来的值改变也会通知其他依赖的,那么为何watchcomputed能拿到异步调用接口的数据而props不行呢?

他们间的区别在哪呢?我觉得应该是Watcher区别,watchcomputed都实例化了Watcher,但是props没有,网上都说可能接口获取到数值时,组件mounted以经执行完了,所以子组件获取不到。咋一听有点道理,这句话说得其实是对的。

我觉得可能分两种情况(都跟异步获取数据有关):
1.子组件在mounted里面拿到props传值并赋值改变,如果父组件通过接口获取,但是拿到数据时子组件mounted已经执行完了,这时候不会再走mounted进行赋值了,所以发现页面是没变化,这是一个时机问题
2.不在mounted里面赋值,但是,编辑进入时,从媒体那边拿到数据的接口有时候非常慢,那么一进入页面第一视觉可能展示的是默认页面,这时候会误认为有问题,但是当接口获取到数据后页面变为正常,所以这里最好做一个loading效果以免错认为bug

但是watch和props的区别在哪里呢?(也可以认为是computed和data区别)
watch和data以及props最大的区别应该是当数据响应式改变时,watch能拿到新旧值,只要发生变化就会触发并拿到新旧值,而data和props只能被动接受值改变,当需要值改变时触发其他方法时,computed以及watch更合适。
例如最近做的易直投,腾讯这条线计划分为:普通计划、微信朋友圈和微信公众号计划,微信朋友圈和普通计划进入单元页对应的区域定向数据源也不一样,都需要通过接口拿到,而编辑进入单元时,首先需要调接口获取计划类型,然后在根据计划类型去获取数据源,这时候我只能通过computed或者watch监听去判断该调哪个接口获取数据。这里就需要watch的回调了。

具体可以看相关源码:



其实源码里面注释也说了,当被watch的值改变时,会提供回调。。。。。。

初识策略模式与项目应用实践

前情

  我终于把 曾探大佬写的《JavaScript设计模式与开发实践》看完了!!!哈哈哈哈哈,但是,我只记得发布订阅模式。。。其他的完全不会,所以要实践啊。然后,前两天项目中又要加新需求,我又要写if了,很气,大佬冬哥说我要好好用用设计模式,比如这个业务代码中的if...else完全可以用 策略模式 来搞!

所以,今天尝试了下并把实践过程记录下。

理论

定义

策略模式:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。

看到这个定义,我不大懂,只不过我对 相互替换 这个词很感兴趣。有没有感觉跟if...else 条件判断很像?if...else 不也是相互可以替换(取代)的嘛。

如何实现?

书上原话:
  一个基于策略模式的程序至少由 两部分 组成。第一个部分是一组策略类,策略类封装了具体的算法(通俗点讲就是我们的业务代码,也就是对应if条件下的业务代码)并负责具体的计算过程。第二个部分是环境类Context,Context接受客户的请求,随后把请求委托给某一个策略类(相当于有个中介在负责派发任务)。要做到这点,说明Context中要维持对某个策略对象的引用。

牢记这些理论,接下来我要用到我的项目中了。

项目实践

背景

一个后台管理系统,大多是列表和与之对应的表单详情页,很常见的。本来没什么,但是列表页有新增、编辑、删除和查看按钮。

其实编辑和查看看到的东西是一样的,进来时都要带id,只是查看进来时,表单详情页需要把保存按钮隐藏掉。其实不难,通过vue-router跳转时加个isCheck参数来确定就好了,同样的,所有表单新建都会在url上带个'create'标识。所以一开始进入form表单的url有以下三种:

// 新建url
http://localhost:8088/#/consultationForm/create
// 编辑url
http://localhost:8088/#/consultationForm/1163054521232949250
// 查看url
http://localhost:8088/#/consultationForm/1163054521232949250?isCheck=true

但是,这个页面有点特别,它里面有个模块选择框,正常这个框的可选项基本都是后台接口返回的,也可以前台配置,这不是重点。

重点是,这个系统还对应一个小程序,小程序中有特定几个模块的功能页面,客户的意思是给特定的几个模块再单独弄几个菜单进行维护。

其实对后台来接口说,不需要动,只要前台画好特定几个模块的列表页,然后请求参数上各个模块写死对应的moduleFlag就好了。至于表单提交页,依然用原先维护所有模块的表单页,只是跳进来需要把模块标识也带进来并禁用不给选。那么url又增加一种情况:

// 运动模块编辑
http://localhost:8088/#/consultationForm/1163054521232949250?moduleFlag=sports

但是我很烦,为啥?

因为,又需要加参数,加判断,内心草泥马奔腾而过,但是又不得不写,然后跟东哥发牢*。东哥说这是我代码还没脱离if...else的境界,然后又点醒我策略模式可以优化。但是那天晚上我赶着提交,没有静下心来思考如何用策略模式重写,附上老代码:

this.$route.params.id !== 'create' && this.initData()
this.isCheck = this.$route.query.isCheck
if (this.$route.query.moduleFlag) {
  this.moduleFlag = this.$route.query.moduleFlag
  this.editFroms[2].value = this.moduleFlag
}

咋一看,好像也没多少条件嘛,但是,万一将来又有什么新需求呢?总不能一直在mounted里面继续加判断吧?

这个最好想个办法优化下代码,不为别的,起码让代码看起来舒服点,维护方便点吧。

趁着今天有点时间,我好好研究了下。翻开《JavaScript设计模式与开发实践》有关策略模式章节,仔细看了看,然后照着修改代码:

created () {
  this.useStrategy('id', this.$route.params.id)
  this.useStrategy('isCheck', this.$route.query.isCheck)
  this.moduleFlag = this.editFroms[2].value = this.useStrategy('moduleFlag', this.$route.query.moduleFlag)
  this.getModuleList()
},
mrthods: {
  /**
    * @description 策略模式应用实践
    */
  strategy () {
    return  {
      'id': (id) => { id !== 'create' && this.initData() },
      'isCheck': (isCheck) => { return !!isCheck },
      'moduleFlag': (moduleFlag) => { return moduleFlag || '' }
    }
  },
  /**
    * @description 使用策略模式
    */
  useStrategy (prop, val) {
    const strategies = this.strategy()
    return strategies[prop](val)
  },
}

总结

能用是能用了,但是我也只是照着书上代码去写,学到了 ,但是策略模式的意境可能还是需要不断实践加理解才行。

策略模式还可以用在表单校验上,不过书上后面介绍的装饰器模式感觉用在校验上更好,等我实践后再说。
不过还是比职责链好,虽然我写代码时把职责链也忘了,不过如果if判断较多,用职责链模式会形成长长的链,这样不好。

策略模式也存在一定的不足,它需要知道应该启用哪种策略?

比如我小程序中有个业务场景,根据接口返回的updateTime当前时间进行比对,不超过60分钟的显示多少分钟前,超过1小时但是未超过24小时的显示多少小时前,超过1天但未超过3天的显示多少天前,超过3天的显示updateTime

这个业务很蛋疼吧。。。但是没办法,就这样要求的

那么我肯定只会传递updateTime进来,如果用策略模式,我并不知道该去调用哪种策略,我不得不拿时间进行比对才能确定策略,这样还不如直接if...else

这种业务的话,职责链模式可能更合适。所以编写代码时,更需要动动脑子,仔细想想,不能拘泥于特定模式。

如何跳出循环?

起因

记得一周前一朋友跟我说forEach没法跳出循环,我当时没怎么在意这个问题,但是留了个心眼,等有机会去研究下。然后昨晚快下班的时候,一同事跑过来问我如何跳出forEach,我立马想到了之前朋友所说的问题,可是因为下班了且时间比较晚,所以劝他先用普通for循环,等到今天早上我在研究下。当我初步尝试时就发现我这js基础还是问题很大啊!

场景

同事做的项目会涉及到大量数据,他需要把数据拿过来然后判断是不是都符合某个条件,如果都符合才能进行下一步操作,一旦出现不符合的数据就要终止整个循环,也不应该进行下面的操作了,所以在这种场景下,跳出循环显得十分必要,都发现有不符合的了下面也没有操作的需要啦,这样一定程度能优化页面加载。

为什么一开始用forEach呢?

因为写代码要*。哈哈哈哈哈~

如果是普通for循环,如何做呢?(我js基础遭受打击了)

1.想当然的用return啊,结果:呵呵!

for (let i = 0; i < 3; i++) {
  console.log(110);
  if (i === 1) {
   return
  }
}

这段代码,我想当然的认为:i === 1时遇到return跳出循环,只会打印两次110,但是当我在控制台按下回车时,啪啪啪打脸啊,脸都打肿了,我还不知道为啥!直接报:Illegal return statement
excuse me? why? why treat me like this???(尼克扬的黑人问号脸脑补下!)

出问题了,那只能去百度或谷歌了。。。

一开始搜索 如何跳出js的for循环?

然后在 https://blog.csdn.net/fxss5201/article/details/52980138 找到了答案!

return语句就是用于指定函数返回的值。return语句只能出现在函数体内,出现在代码中的其他任何地方都会造成语法错误!

竟然还有这种限制,为何我以前怎么没发觉,仔细看了下以前的代码,貌似,我的for都是写在函数内的,所以没报错。。。

当然,为了保险点,又去MDN看了下return,果不其然,MDN上抬头就是一句:return语句终止函数的执行,并返回一个指定的值给函数调用者。

更保险的可以看ECMAScript® 2015 Language Specification return。规范里面也说了返回语句使函数停止执行,并将值返回给调用方。

2.既然return不是为for服务的,那么该怎么去跳出for循环呢?

想必基础牢固的同学们立马就能说出breakcontinute这两兄弟。不过我这种基础不扎实的学渣只能去百度以及看MDN和规范了,直接在MDN上找for,可能MDN觉得这种东西太简单了,直接用了示例就告诉我们break能跳出循环。

这里顺便带上ECMA规范里的for

MDN上关于break的说明:break 语句中止当前循环,switch语句或label 语句,并把程序控制流转到紧接着被中止语句后面的语句。break可不仅仅是跳出循环这么简单哦~

一样的,顺带附上规范里的break

for (let j = 0; j < 3; j++) {
  console.log(112);
  if (j === 1) {
    break;
  }
}

3.我在搜索的过程中,发现网上经常同时拿breakcontinue举例,用法大家肯定都很熟悉。试一下就知道了。

for (let j = 0; j < 3; j++) {
  if (j === 1) {
    continue;
  }
  console.log(j);
}

continue会跳出符合条件的当次循环,但是会继续执行接下来不符合条件的循环,说的有点绕了,大家结合代码自行理解。附上MDN continue以及规范中的continue

以上就是关于跳出普通for循环的知识了,那么,接下来我得去看看为什么不能跳出forEach

为什么不能跳出forEach

1.先用breakcontinue分别测试一波,看看问题是否成立。

arr.forEach(function(item){
  if (item === 2) {
    break;
  }
  console.log(item);
})
arr.forEach(function(item){
  console.log(110)
  if (item === 2) {
    continue;
  }
  console.log(item);
})

控制台均报语法错误!!!尤其continue提示没有包含在一个迭代语句中!!!

why???

去看看MDN上面的forEach,其中明确提到:

没有办法中止或者跳出 forEach 循环,除了抛出一个异常。如果你需要这样,使用forEach()方法是错误的,你可以用一个简单的循环作为替代。如果您正在测试一个数组里的元素是否符合某条件,且需要返回一个布尔值,那么可使用 Array.everyArray.some。如果可用,新方法 find() 或者findIndex() 也可被用于真值测试的提早终止。

同时,去看看规范里的forEach。貌似没有说为什么。那么怎么办呢?

回顾continue报错提示,需要被包含在迭代语句内,难道forEach不是迭代语句吗?它作用不就是遍历吗?抱着这些问题,我在规范的目录里走马观花的查看,突然发现这个Iteration Statements,这个目录名不就是continue要求的迭代语句嘛。

我点了下去,大致看了下,果然没有forEach,而且mapfilter等都没有。。。

我有尝试去看v8关于forEach的源码实现,可惜并没有找到。。。

不过这里可以提供一个仿map方法的_map,本质上是一样的,只是侧重点不同,有些实现不同。我们平常这样使用:arr.forEach(function(){}),我们都是在function(){}中去做我们需要的操作,但是,forEach本质上就是一个函数,而我们写的函数其实是传递给它调用的回调函数!!!

所以,当我在回调函数内写了return,即使符合条件也只是跳出回调函数而已,并没有跳出调用它的forEach

不理解的话看个简单例子,比较直观:

function a(i) {
  console.log(i);
  if(i === 1) {
    return
  }
}
function b(arr) {
  for(var i = 0; i < arr.length; i++) {
    a(i)
  }
}
var arr = [1,2,3,4,5]
b(arr)

i === 1时,并没有跳出函数b

总结下

由这一个小问题挖掘出这么多小知识也是不错的,有利于夯实js基础。以后遇到类似的业务场景时,该用普通for就用普通for吧,不要老想着*操作。当然,如果一定要*一把,可以抛异常:

let arr = [1, 2, 3]
arr.forEach(function(item){
  console.log(110)
  if (item === 2) {
    throw Object.create({'error': '不符合'})
  }
  console.log(item);
})

重学js —— js数据类型:Null、Undefined、Boolean

Null 类型

null
typeof null // "object" 
Object.prototype.toString.call(null) // "[object Null]"
Boolean(null) // false
!!null // false
Number(null) // 0
+null // 0
~null // -1
~~null // 0
  1. 以上代码,类型转换可以详见 重学js —— 类型转换
  2. 按位非 ~ 运算详见 重学js —— 一元运算符(delete/void/typeof/+/-/~/!)

Undefined 类型

MDN上有这么一句话:undefined是全局对象的一个属性。也就是说,在浏览器环境下,它是 window 对象的属性,更是是一个全局变量!!!。

这是真的嘛?看图说话:

结合规范可知,全局对象上的 undefined 属性不可遍历,不可配置,也不可写!!!(据说远古时期,undefined 是可以被赋值改变的,好在现在没有这个坑了)

Undefined 与 Null 区别?

  1. 类型不同,都属于js基本数据类型
  2. 转为 Number 时值不同
  3. 转为 Boolean 时相同
  4. 转为 String 时不同
  5. 转为 Object 时都是空对象
  6. undefined 还是全局对象的属性,不可配置修改以及遍历
  7. void 运算时会返回 undefined
  8. 使用 var, let 声明的变量,未赋值情况下,默认初始值为 undefined
  9. 如果函数没有显式的返回值,那么执行函数后打印输出 undefined
  10. 函数形参,如果没有接收到值,默认为 undefined

通过代码表现看异同:

// 比较,类型不同
null == undefined // true
null === undefined // false

// typeof操作符
typeof null // 'object'  js中所有对象原型链的终点是 null
typeof undefined // 'undefined'

// Object.prototype.toString方法 涉及到js内置对象
// 可以看 https://github.com/lizhongzhen11/lizz-blog/issues/1
Object.prototype.toString.call(null) // '[object Null]'
Object.prototype.toString.call(undefined) // '[object Undefined]' 

// + 转换,转为 `Number` 时值不同
+null // 0
+undefined // NaN  注意!!!

// ~~ 转换
// 见 https://github.com/lizhongzhen11/lizz-blog/issues/72
~~null // 0
~~undefined // 0  注意!!!

// void操作
// 见 https://github.com/lizhongzhen11/lizz-blog/issues/72
void null // undefined

// 注意,未定义或未声明的变量,既不是null也不是undefined
a // Uncaught ReferenceError: a is not defined

function test(a){
    console.log(a);    // undefined
    return a;
}
test(); // undefined

如上最后的代码,直接打印变量a报错,但是作为函数参数,没有传参进去,会默认打印 undefined,这是为何呢?

a // 报错

function test(b) {
  console.log(b) // undefined
}
test() // undefined

function test2() {
  console.log(c) // 报错
}
test2() // 报错

如上test中,形参b到底算不算局部变量?如果算,那为何它可以不用声明就有默认的undefined值,反观a和c却报错呢?

网上不少文章认为形参b属于test函数的局部变量,会在test函数内部顶层声明,所以会打印undefined
翻译成代码大致是这样:

function fun(b){
  var b
  b = arguments[0]
  console.log(b)
}

可以看:

我仔细研究了下函数创建过程,生成 函数对象 时会有个 [[FormalParameters]] 内置插槽来保存形参,包括 [[Call]] 的内部算法,并没有看到具体形参赋值情况。

Boolean 类型

Boolean(Boolean) // true  Boolean其实是个构造函数,属于对象!
new Boolean() // Boolean {false}
Object.getPrototypeOf(Boolean) === Function.prototype // true

// Boolean.prototype对象是内置的 %BooleanPrototype% 对象,它不是Boolean对象实例
// Boolean.prototype.toString 以及 Boolean.prototype.valueOf 依赖于实例的传值
new Boolean().toString() // 'false'
new Boolean(true).toString() // 'true' 
+[] // 0
Boolean([]) // true; []属于Object,直接返回true

读《webkit技术内幕》了解浏览器内核二——内部架构

前言

跟着读《webkit技术内幕》了解浏览器内核一,我对各大主流浏览器有了大致了解,通过图例对渲染过程也有了初步认知,接下来,我会跟着书去认识下webkit架构和模块

问题

webkit内部是什么样的?或者说内部架构是什么样的?

还是看图,有些时候图比文字直观:

注意:

虚线框表示该部分模块在不同浏览器使用的WebKit内核中的实现是不一样的,也就是它们不是普遍共享的。

根据图中列出的模块一一说明下:

  • 最下面的“操作系统”——WebKit可以在不同的操作系统上工作。
  • 在“操作系统”层之上的就是WebKit赖以工作的众多第三方库,这些库是WebKit运行的基础。
  • 在它们二者之上就是WebKit项目了,图中把它细分为两层,每层包含很多模块,图中略去了其中一些次要模块。这些模块支撑了网页加载和渲染过程。
  • WebCore部分包含了目前被各个浏览器所使用的WebKit共享部分,这些都是加载和渲染网页的基础部分。
  • JavaScriptCore引擎是WebKit中默认的JavaScript引擎。
  • WebKit Ports指的是WebKit中非共享部分。
  • 嵌入式编程接口,提供给浏览器调用的。图中有左右两个部分分别是狭义WebKit接口和WebKit2接口。因为接口与具体的移植有关,所以有一个与浏览器相关的绑定层。
  • ...

那么WebKit源代码结构是什么样的呢?

看:https://github.com/WebKit/webkit ,开源代码库

注意:由于WebKit项目依然在发展,所以和书中的介绍会有点区别。

现在知道了WebKit的结构,那些基于WebKit内核的浏览器的结构是什么样的?

以Chromium为例,看图了解:

一样的,根据图例来大致了解下每个模块功能:

  • Content模块和Content API,它们是Chromium对渲染网页功能的抽象(这里是指用来渲染网页内容的模块。PS:注意和WebKit区别)。
  • Content模块和Content API将下面的渲染机制、安全机制和插件机制等隐藏起来,提供一个接口层。
  • Chromium浏览器和Content Shell是构建在Content API之上的两个“浏览器”,Chromium具有浏览器完整的功能,也就是我们编译出来能看到的浏览器式样。而Content Shell是使用Content API来包装的一层简单的“壳”,它也是一个简单的“浏览器”,用户可以使用Content模块来渲染和显示网页内容。

Content模块渲染网页内容,可是WebKit本身不就是渲染网页的吗,为什么Blink里面有两个渲染网页的模块???

即使没有Content模块,浏览器开发者也可以在WebKit的Chromium移植上渲染网页内容,但是却没有办法获得沙箱模型、跨进程的GPU硬件加速机制、众多的HTML5功能,因为这些都是在Content层里实现的。

在了解了基本架构和模块功能后,我们肯定很好奇浏览器是如何将这些模块连接起来并正常运行的?整个运行过程是一条直直的流水线吗(单线进行?)?是不是所有的功能都是循序渐进依次进行的(同步?)?
...

随着了解的更多,产生的问题也更多!

整个运行过程是单线的?

我提出这种问题时,其实是基于js单线程这个特点来引申的。对于没有计算机基础的人来说,学习这些可能连问题提的都切不到要点,所以我只能用联想到的往上套。

根据多年上网经历,以前在网吧上网,打开某个网页,由于某些原因导致该页面崩溃了,随之而来的可能是其他所有页面都崩溃甚至浏览器也崩掉了,这种经历真的很令人气愤。但是,这也说明了,远古时期,浏览器确实是单进程的!

而现在,很多现代浏览器支持多进程模型,是多进程的!!!

什么是进程?什么是线程?

上面我引入了进程线程这两个术语,建议如果和我一样是转行且没有计算机基础的人,有时间的话看《深入理解计算机系统》,补下基础。

引用《深入理解计算机系统》:

进程是操作系统对一个正在运行的程序的一种抽象。在一个系统上可以同时运行多个进程,而每个进程都好像在独占地使用硬件。而并发运行,则是说一个进程的指令和另一个进程的指令是交错执行的。在大多数系统中,需要运行的进程数是多于可以运行它们的CPU个数的。无论在单核还是多核系统中,一个CPU看上去都像是在并发地执行多个进程,这是通过处理器在进程间切换来实现的,操作系统实现这种交错执行的机制称为上下文切换

多进程模型有什么好处?

至少3点好处:

  1. 避免因单个页面的不响应或者崩溃而影响整个浏览器的稳定性,特别是对用户界面的影响。
  2. 当第三方插件崩溃时不会影响页面或者浏览器的稳定性,这是因为第三方插件也被使用单独的进程来运行。
  3. 它方便了安全模型的实施,也就是说沙箱模型是基于多进程架构的。(WebKit2也引入了多进程)

多进程由哪些进程组成呢?

Chromium最常用的多进程模型图例:

根据图例,分别解释下:

  • Browser进程:浏览器的主进程,负责浏览器界面的显示,各个界面的管理,是所有其他类型进程的祖先,负责它们的创建和销毁等工作,有且仅有一个
  • Renderer进程:网页的渲染进程,负责页面的渲染工作,Blink/WebKit的渲染工作主要在这个进程中完成,可能有多个,Renderer进程数量和用户打开的网页数量并不一致
  • NPAPI进程:为NPAPI类型的插件而创建的。
  • GPU进程:最多只有一个。当且仅当GPU硬件加速打开时才会被创建。主要用于对3D图形加速调用的实现。
  • Pepper插件进程:同NPAPI进程,不同的是为Pepper插件而创建的进程。
  • 其他类型的进程

Chromium的进程模型有哪些特点?

  1. Browser进程和页面渲染是分开的,这保证了页面的渲染导致的崩溃不会导致浏览器主界面的崩溃。
  2. 每个网页是独立的进程,这保证了页面之间相互不影响。
  3. 插件进程也是独立的,插件本身的问题不会影响浏览器主界面和网页。
  4. GPU硬件加速也是独立的。

Browser进程和Renderer进程是如何利用WebKit渲染网页的?

看图说话:

通过上述若干问题,有了些大致了解。那么,进程内部又是什么情况呢?如何在支持进程间通信的同时又能支持高效渲染或者用户事件响应?

答案是 多线程模型

什么是多线程模型?

每个进程内部,都有很多的线程。

Chromium为什么要用多线程?

主要目的就是为了保持用户界面的高响应度,保证UI线程(Browser进程中的主线程)不会被任何其他费时的操作阻碍从而影响了对用户操作的响应。
而在Renderer进程中,Chromium则不让其他操作阻止渲染线程的快速执行。

能不能举个例子来大概说明一下多线程是如何工作的?

以IO操作来举例,看图:

根据图示来理解:

  1. Browser进程收到用户的请求,首先由UI线程处理,而且将相应的任务转给IO线程,它随即将任务传递给Renderer线程。
  2. Renderer进程的IO线程经过简单解释后交给渲染线程。渲染线程接受请求,加载网页并渲染,这其中可能需要Browser进程获取资源和需要GPU进程来帮助渲染。最后Renderer进程将结果由IO线程传递给Browser进程。
  3. 最后,Browser进程接收到结果并将结果绘制出来。

那么,Chromium中的线程间如何通信和同步呢?

这是多线程领域中一个非常难缠的问题,因为这会造成死锁或者资源的竞争冲突等问题。

Chromium设计了一套机制来处理它们,那就死绝大多数场景使用事件一种Chromium新创建的任务传递机制,仅在非用不可的情况下才使用锁或者线程安全对象。

待续

本篇介绍了WebKit架构和模块,以及了解了Chromium的多进程、多线程模型。尤其在多线程这块,还可以深入,引出很多问题,不过,这前提是我们对浏览器内部其他机制也有一定了解才能去深入。

读《webkit技术内幕》了解浏览器内核系列

  1. 读《webkit技术内幕》了解浏览器内核一
  2. 读《webkit技术内幕》了解浏览器内核二——内部架构
  3. 读《webkit技术内幕》了解浏览器内核三——资源与网络栈
  4. 读《webkit技术内幕》了解浏览器内核四——HTML、DOM以及CSS相关
  5. 读《webkit技术内幕》了解浏览器内核五——渲染基础
  6. 读《webkit技术内幕》了解浏览器内核六——硬件加速机制
  7. 读《webkit技术内幕》了解浏览器内核七——js引擎

字符编码科普

字符编码科普

字符编码系列——撒网要见鱼(必看)

刨根究底字符编码系列(最好看完)

UTF-8 往事

知乎ASCII话题

知乎Unicode话题

知乎字符编码话题

在了解之前,需要先懂一点计算机基础知识:

  • 计算机中最小数据单位是 bit (也叫 )。

  • bit是信息量单位。二进制的一位,就叫做 1 bit。也就是说 bit 的含义就是二进制数中的一个数位,即 0 或者 1。因此,bit只有两种状态:0 和 1

  • 一般来说,n比特的信息量可以表现出 2的n次方 种选择。

  • 计算机其实只能识别二进制代码,即 0 和 1。但是,计算机最小存储单位是 字节。同时,1字节 = 8位。即1字节可表示的二进制数范围:00000000 ~ 11111111,2的8次方即256种选择,对应十进制 0 ~ 255。一个字节能表示的最大的整数就是255。

附上二进制与十进制和十六进制之间的对应关系:

ASCII

ASCII是一种标准的 单字节 字符编码方案,用于基于文本的数据,而其码位是 00000000 ~ 01111111,即2的7次方共 128 个字符,0 ~ 127。(最大范围取决于1)

后面介绍的字符集,有的会兼容ASCII,所谓的兼容,其实就是保持其“编号不变”。

这也就导致了在js string比较字符串大小时,很多人往往会认为是按照ASCII排序来比较的。其实只是因为js string采用的字符编码兼容了ASCII码,但本质上应该是Unicode字符集才对(ES6之前是UCS-2,不过UCS-2又是UTF-16的子集)。

UCS 通用字符集

ISO发布的,后来与统一码联盟一起合作,UCS-2和Unicode兼容。

它有UCS-2和UCS-4两种格式,分别是2字节和4字节。

范围:UCS-4只是在UCS-2前面加了0×0000。

虽然现在两个项目仍都存在,并独立地公布各自的标准,但统一码联盟和ISO/IEC都同意保持两者的通用字符集相互兼容,并共同调整未来的任何扩展。

Unicode

由统一码联盟设计发布。

Unicode的知名度要比UCS知名度大得多,已成了全球统一的通用字符集或编码方案的代名词。并且在实践中,Unicode也要比UCS应用得更为广泛得多。因此,Unicode字符编码方案已经成为了全球统一字符编码方案事实上的标准。

Unicode是一个标准,只规定了某个字符应该对应哪一个code,但是并没有规定这个字符应该用几位字节来存储。规定用几个字节存储字符的是Unicode的不同实现,譬如UTF-8,UTF-16等。

UTF-8是Unicode的一种实现方式,是一种变长编码,根据不同的Unicode字符,用 1到6个字节 编码,完全兼容ASCII。目前,UTF-8几乎一统天下。(一般用二进制)

UTF-16编码方式有两种,范围应该在 U+0000 ~ U+FFFF ,对应二进制 00000000 00000000 ~ 11111111 11111111, 即2的16次方种选择,对应 0 ~ 65535 内的字符用 两个字节 表示,超过的用4个字节。不完全兼容ASCII(毕竟UTF-16最低也是2字节,而ASCII则是单字节的)

UTF-16是完全对应于 UCS-2 的,算是UCS-2的父集。UTF-16也可以表示UCS-4的部分字符,所以UTF-16也采用4个字节来存储Unicode。

在表示一个 UTF-16 的字符时,通常会用 U+ 然后紧接着一组十六进制的数字来表示这一个字符。例如 U+0000.

基本多文种平面 BMP

目前,Unicode字符集将所有字符按照使用上的频繁度划分为17个平面(Plane),每个平面上的编号空间有2^16=65536个码点。

基本多文种平面也叫第零平面(Plane 0),是17个平面中的第一个。编码从 U+0000至U+FFFF 。基本涵盖了当今世界上正在使用中的常用字符,平常用到的Unicode字符,一般都是位于BMP平面上的。

BMP平面以外其他的增补平面SP(Supplementary Plane,也称为辅助平面)要么用来表示一些非常特殊的字符(比如不常用的象形文字、远古时期的文字等),且多半只有专家在历史和科学领域里才会用到它们;要么被留作扩展之用。

GB2312

GB是”国标”两字的拼音首字,2312是标准序号。

GB2312编码是第一个汉字编码国家标准,由**国家标准总局1980年发布,1981年5月1日开始使用。

采用双字节编码。

GBK

是对GB2312编码的扩展,因此完全兼容GB2312-80标准。采用双字节编码方案,其编码范围:8140-FEFE,剔除xx7F码位,共23940个码位。

BIG5

BIG5编码又称大五码,是繁体中文字符集编码标准

GB18030-2005

采用单字节、双字节和四字节三种方式对字符编码。兼容GBK和GB2312字符集。完全兼容ASCII码与GBK码。

主要看开头链接

读《JavaScript设计模式与开发实践》学习 装饰者 模式

理论

给对象动态地增加职责的方式称为装饰者模式

业务中的应用

我认为最佳实践就是表单校验。

最一开始,我这样写表单校验:

// 伪代码
if (条件) {
  console.log('不符合条件。。。')
  return false
}
if (条件) {
  console.log('不符合条件。。。')
  return false
}
...
ajax() // 提交

后来随着业务写多了,慢慢有点代码审美了,这种将校验和保存提交请求写在一个函数内部,简直垃圾的一笔!!!

一旦校验增加、变动就要去改内部代码,而且明显违反了单一职责原则。

我渐渐的,会将校验和提交分成两个函数来写了:

// 伪代码
validate () {
  if (条件) {
    console.log('不符合条件。。。')
    return false
  }
  ...
  return true
}
submit () {
  if (!validate()) {
     return
  }
  ajax()
}

但是,碰到复杂表单/校验较多的表单,validate函数内部依然会有一大堆if...else代码,然后可能这个函数达到数十行,还是十分垃圾,这里面可以结合之前学到的策略模式进行优化。

以前我只做到这一步,然后就没啥更好的想法了。但是,看书发现,还可以继续使用装饰者模式进行优化。

// 伪代码
Function.prototype.before = function (beforeFn) {
  let self = this
  return function () {
    if (beforeFn.apply(this, arguments)) {
      return self.apply(this, arguments)
    }
    return 
  }
}
function validate () {
  if (条件) {
    console.log('不符合条件。。。')
    return false
  }
  ...
  return true
}
function submit () {
  ajax()
}
submit = submit.before(validate)

不污染原型的写法:

function before (func, beforeFunc) {
  return function () {
    if (beforeFunc.apply(this, arguments)) {
      retrurn func.apply(this, arguments)
    }
    return
  }
}
let res = before(submit, validate)
res()

读《数据结构与算法 JavaScript描述》——实现一个散列表上--初识散列表不处理碰撞

理解散列表

散列表到底是什么?

其实以前或多或少接触过这个名词,但是抱歉,我真不知道它是什么?它干嘛用的?写这篇文章时,我还特地去百度百科上看了下:

散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

还是有点饶人吧,但是抓下重点:存放记录的数组叫做散列表。数组?也就是说散列表本质还是数组!并且,散列表就是哈希表!

可以简单地把散列表定义为:使用散列函数和数组创建了一种数据结构

散列表有什么用?

在散列表上插入、删除和取用数据都非常快,但是对于查找操作来说却效率低下,比如查找一组数据中的最大值和最小值。查找的这些操作得求助其它数据结构,二叉查找树就是一个很好的选择。

案例介绍:https://cxyxiaowu.com/articles/2019/04/08/1554728305261.html#b3_solo_h3_6

如何去实现散列表结构?

从上可知,实现散列表结构首先得有个对应的散列函数

同时,还需要考虑:散列表中的数组究竟应该有多大?这是编写散列函数时必须要考虑的。
对数组大小常见的限制是:数组长度应该是一个质数

为什么数组长度应该是一个质数?

首先,散列函数的选择依赖于键值的数据类型。如果键是整型,最简单的散列函数就是以数组的长度对键取余。假设数组长度是10,而键值刚好都是10的倍数时,对键取余后会出现大量冲突,没有起到散列效果,不合理。

如何选择散列函数?

  1. 如果像上文一样,键值是整型,直接用数组长度对键取余,这种方法被称为除留余数法
  2. 但是再很多应用中,键是字符串类型,选择散列函数相对较难。由于是初级入门版本,可以选择将字符串中每个字符的ASCII码值相加来实现散列函数。这样散列值就是ASCII码值的和除以数组长度的余数

实现代码(来自书上):

// 散列表
function HashTable () {
  this.table = new Array(137);
  this.simpleHash = simpleHash;
  this.showDistro = showDistro;
  this.put = put
}
// 散列函数
function simpleHash (data) {
  let total = 0;
  for (let i = 0; i < data.length; ++i) {
    total += data.charCodeAt(i);
  }
  return total % this.table.length;
}
// 插入方法
function put (data) {
  let pos = this.simpleHash(data); // 获取散列值
  this.table[pos] = data;
}
function showDistro () {
  let n = 0;
  for (let i = 0; i < this.table.length; i++) {
    if (this.table[i] !== undefined) {
      console.log(i + ': ' + this.table[i])
    }
  }
}
// 测试
let someNames = ['David', 'Jennifer', 'Donnie', 'Raymond', 'Cynthia', 'Mike', 'Clayton', 'Danny', 'Jonathan']
let hashTable = new HashTable()
for (let i = 0; i < someNames.length; i++) {
  hashTable.put(someNames[i])
}
hashTable.showDistro()

// 结果
35: Cynthia
45: Clayton
57: Donnie
77: David
95: Danny
116: Mike
132: Jennifer
134: Jonathan

存在的问题

上述代码示例中,数组中明明有9条数据,但是只打印出8条,这是为何呢?

改写下simpleHash函数:

function simpleHash (data) {
  let total = 0;
  for (let i = 0; i < data.length; ++i) {
    total += data.charCodeAt(i);
  }
  console.log('Hash value: ' + data + ' -> ' + total);
  return total % this.table.length;
}

然后再次运行得到如下结果:

Hash value: David -> 488
Hash value: Jennifer -> 817
Hash value: Donnie -> 605
Hash value: Raymond -> 730
Hash value: Cynthia -> 720
Hash value: Mike -> 390
Hash value: Clayton -> 730
Hash value: Danny -> 506
Hash value: Jonathan -> 819

从结果可知,Raymond 和 Clayton 两的散列值一样,这就形成了冲突(碰撞),说明目前的散列函数并不完美,需要改善以避免碰撞。不过这章不讲,待下面更新。

读《JavaScript设计模式与开发实践》学习 发布-订阅 模式

2019-12-06 更新

说来惭愧,时隔半年,我居然给忘了,居然一时想不起来怎么写了。。。
不过这次愣是没有看任何文章,自己在本地慢慢猜想+代码实践勉强写出来了,代码和经历在:utils/on.js

啰嗦几句

  由于自己体质不强加上之前一直加班,结果查出了一种病,为了自己的未来,想想还是从南京撤了。不值得,一年也就挣那么点钱。。。5.2号回来的,在家玩了一个月的梦幻西游了,真好玩!
  接下来可能会去扬州市区找工作,目前只会开发,能找个不加班的就好,找不到的话也许在镇上厂里找个文职类工作了。自己有注册个小工作室,只是没有接单来源,考虑自身实力不够硬,尤其是后端数据库那块,如果自己一个人独立开发完整项目是不行的,目前了解到扬州基本上在招前后端都能写的,有点小凉。。。
  不管接下来干什么,技术这块还是不能随意丢弃的,买了很多书都没看呢,所以现在要求自己早上看书,下午才能玩游戏,哈哈。
  《JavaScript设计模式与开发实践》这本书还是很棒的,发布-订阅模式其实开发中经常遇到,不过大多都是人家封装好的,难得静下心来去研究研究,之前面试蚂蚁金服时就要求我手写,可惜菜啊,不会啊,今天早上把书上内容看完,然后自己凭着理解去写、实践,还是有点小收获的,满满的自豪感~~~

附上代码

var Subscribe = (function () {
  let subscriber = {}, // 缓存订阅者和它订阅的信息类型;
      listen, // 添加订阅者
      publish, // 发布数据
      remove // 移除订阅

  /**
   * @param {key} 订阅的信息类型
   * @param {fn} 订阅者的回调函数
   * @description 添加订阅者(订阅事件)
   */
  listen = function (key, fn) {
    if (!subscriber[key]) {
      subscriber[key] = []
    }
    subscriber[key].push(fn)
  }

  /**
   * @description 发布执行 
   */
  publish = function () {
    let key = Array.prototype.shift.call(arguments), // 得到订阅的信息类型
        fns = subscriber[key] // 对应的订阅回调函数
    // 没有对应的订阅事件
    if (!fns) {
      return false
    }
    // 对该订阅类型下的回调函数全部派发执行
    for (let i = 0, fn; fn = fns[i++];) {
      fn.apply(this, arguments)
    }
  }

  /**
   * @param {key} 订阅类型/标识
   * @param {fn} 需要移除的订阅回调函数
   * @description 删除订阅
   */
  remove = function (key, fn) {
    let fns = subscriber[key] // 对应的订阅回调函数集合
    if (!fns) {
      return false
    }
    // 没有指定要移除的订阅回调函数,则将对应类型下的回调函数全部移除
    if (!fn) {
      fns.length = 0
    } else {
    // 指定了需要移除的订阅回调函数
      for (let i = 0; i < fns.length; i++) {
        if (fns[i] === fn) {
          fns.splice(i, 1)
        }
      }  
    }
  }

  return {
    subscriber: subscriber,
    listen: listen,
    publish: publish,
    remove: remove
  }
})()

Subscribe.listen('a', function (data) {
  console.log('a: ' + data)
})

Subscribe.publish('a', 20000)

读《webkit技术内幕》了解浏览器内核七——js引擎

终于来到心心念念的js引擎篇了。
js引擎很复杂!很重要!

书中主要介绍的是JavaScriptCore引擎和V8引擎.

首先,怎么理解js是动态类型语言?

有动态就有静态。动态和静态语言的主要区别在于编译过程。

以java为例,java是静态类型语言,java声明一个类以及里面的变量是这样的:

public class Test {
  public int num;
}

也就是当java开发者在写一个类时,这个类里面所有的变量、方法等都已经确定好了类型,那么java编译时就不需要再去进行类型判断了。所以说java是静态类型语言。

而js则恰恰相反,js写的很爽,根本不需要去考虑所谓的类型声明,写的时候起飞,但是当真正项目运行时,不得不对各种类型转换小心再小心。js编译时,js引擎必须要对每行代码中的每个变量、方法去进行类型判断。(虽然目前有了TypeScript,不过这并不是规范,只是微软开发的超集,但是却让js开发书写的更加规范)

那么js的运行效率比之java如何呢?

仅仅根据动静态类型来分析,一般情况下js运行效率会比java低很多。
(各大引擎都会对js编译运行进行各种优化的)

为什么说js运行效率会比不过静态类型的java?

示例:

public int add(int a, int b) {
  return a + b
}

以这段代码为例,首先 int占4个字节,上述代码其实就是 a的内存地址+4个字节,这些是在生成本地代码时候就确定了。根据这些信息就能生成相应的汇编代码。

而传统的js解释器,所有这一切都是解释执行的,所以效率不会高到哪去。不管是解释器还是更为高效的 JIT(Just-In-Time) 技术,面临的难题都是类型问题。

上面提到了更为高效的JIT技术,它是什么?为何更高效?

首先,它不是一项新技术,其作用是解决解释性语言的性能问题,主要**是当解释器将源代码解释成内部表示的时候(java字节码就是一个典型例子),JavaScript的执行环境不仅是解释这些内部表示,而且 将其中一些字节码(使用效率高的)转成本地代码(汇编代码),这样可以被CPU直接执行,而不是解释执行,从而极大地提高性能。

经过上面了解,回到本篇中心——js引擎

什么是js引擎呢?

简单来讲,能将js代码处理并执行的运行环境
PS:要理解这一概念,需要了解一些编译原理的基础概念和现代语言需要的一些新编译技术。

js引擎解析js代码的步骤是什么呢?

早期:js源代码——抽象语法树(AST)——解释器解释执行
引入JIT后看图(不同的引擎选择的方法不同,所以总体来看很复杂)

从上图所知,js引擎应该包括哪些部分?

  • 编译器:源代码——抽象语法树(某些引擎中还会将抽象语法树转成字节码)
  • 解释器:主要接收字节码,解释执行这些字节码,同时依赖垃圾回收机制等
  • JIT工具:字节码 / 抽象语法树——本地代码(汇编代码)
  • 垃圾回收器和分析工具:垃圾回收和收集引擎中的信息,帮助改善引擎的性能和功效

js引擎和渲染引擎是什么关系?

从模块上看,它们彼此独立,负责不同的事情;
js引擎提供调用接口给渲染引擎,以便让渲染引擎使用js引擎来处理js代码并获取结果。
js引擎需要能够访问渲染引擎构建的DOM树,所以js引擎通常需要提供桥接的接口,而渲染引擎则根据桥接接口来提供让js访问DOM的能力。
看图

注意:两种引擎通过桥接接口来访问DOM结构,这对性能来说是一个重大的损失,因为每次js代码访问DOM都需要通过复杂和低效的桥接接口来完成(毕竟js是单线程的)

什么是V8引擎?

V8是一个开源的js引擎项目,github地址:https://github.com/v8/v8
一开始是由一些语言方面的专家设计出来的,后来被Google收购,目前是js引擎和众多相关技术的引领者。
目的就是为了 提高性能

为了提高js执行效率,甚至采用直接将js编译成本地汇编代码的方式。

想了解v8的代码的,建议阅读include和src文件夹。

V8提供了哪些应用程序编程接口(API)呢?

主要在include/v8.h中。

  • 各种基础类:对象引用类、基本数据类型类和js对象。都是基础的抽象类,具体实现在src/objects中
  • Value:所有js数据和对象的基类
  • V8数据的句柄类:以上数据类型的对象在V8中有不同的生命周期,需要是用句柄来描述它们的生命周期,以及垃圾回收器如何使用句柄来管理这些数据
  • Isolate:V8引擎实例
  • Context:执行上下文,包含内置的对象和方法
  • Extension:扩展类
  • Handle:句柄类,管理基础数据和对象,以便垃圾回收器操作
  • Script:表示被编译过的js源代码,V8内部表示
  • HandleScope:包含一组Handle的容器类
  • FunctionTemplate:绑定C++函数到js。例如将js接口的C++实现绑定到js引擎中
  • ObjectTemplate:绑定C++对象到js。典型应用Chromium中将DOM节点通过该模板包装成js对象

js引擎的工作原理是什么?

主要分为以下几个部分:

  • 数据表示
  • 工作过程
  • 优化回滚
  • 隐藏类和内嵌缓存
  • 内存管理
  • 快照

V8中怎么进行数据表示的?

分成两个部分:

  • 数据的实际内容
  • 数据的句柄。

V8需要进行垃圾回收,并需要移动这些数据内容,如果直接使用指针(引用)的话就会出现问题或者需要比较大的开销,使用句柄的话就不会存在这些问题,只需要将句柄中的指针修改即可,使用者使用的还是句柄,它本身没有发生变化。

一个Handle对象大小是4字节(32位系统)或8字节(64位系统),而在JavaScriptCore引擎中使用8个字节来表示数据的句柄。
看图

V8的工作过程是什么样的?

两个阶段:先编译后执行

V8有个非常重要的特点就是 延迟**,很多js代码直到运行时被调用才会进行编译,这样会减少时间开销。

编译过程

看下V8引擎处理源代码到本地代码的过程图:

V8不同于JavaSrciptCore在于,V8并不将抽象语法树变成字节码或其他中间表示,而是通过JIT编译器的全代码生成器(full code generator)从抽象语法树直接生成本地代码。
这样做主要是因为减少抽象语法树到字节码的转换时间。

V8编译js生成本地代码主要使用了以下类:

  • Script:表示js代码,既包含源代码,又包含编译后生成的本地代码,所以既是编译入口,又是运行入口
  • Compiler:编译器类,辅助Script类来编译生成代码,它主要起一个协调者的作用,会调用解释器(Parser)来生成抽象语法树和全代码生成器,来为抽象语法树生成本地代码
  • Parser:将源代码解释并构建成抽象语法树,使用AstNodeFactory类来创建它们,并使用Zone类来分配内存
  • AstNode:抽象语法树节点类,是其他所有节点的基类
  • AstVisitor:抽象语法树的访问者类,基于著名的设计模式Visitor来设计,主要用来遍历异构的抽象语法树
  • FullCodeGenerator:AstVisitor类的子类,通过遍历抽象语法树来js生成本地可执行代码(不同平台不同实现)

根据上面类的描述,大致总结这样一个编译js代码的过程:
Script类调用Compiler类的Compiler函数为其生成本地代码。在该函数中,第一,它使用Parser类来生成抽象语法树;第二,使用FullCodeGenerator类遍历生成的抽象语法树从而生成本地代码

根据前面的 延迟**,事实上,js中很多函数是没有被编译生成本地代码的。因为js代码编译之前需要构建一个运行环境,所以实际上在编译前,V8引擎会构建众多全局对象并加载一些内置的库,如math库等。

V8在生成本地代码后,为了性能考虑,通过数据分析器(Profiler)来采集一些信息,以帮助决策哪些本地代码需要优化,以生成效率更高的本地代码。同时,V8还有一种机制,也就是当发现优化后的代码性能其实并没有提高甚至还有所降低,那么V8能够退回到原来的代码。

执行过程

看下运行阶段的主要类:

  • Script:同上
  • Execution:辅助类,包含一些重要的函数,例如Call函数
  • JSFunction:需要执行的js函数表示类
  • Runtime:运行本地代码的辅助类。提供运行时各种各样的辅助函数,包括但不限于属性访问、类型转换、编译、算术、位操作、比较、正则表达式等。
  • Heap:运行本地代码需要使用的内存堆
  • MarkCompactCollector:垃圾回收机制的主要实现类
  • SweeperThread:负责垃圾回收的线程

简单说下执行过程:
第一,延迟编译。
  也就是 CompileLazy 函数的调用,根据需要编译和生成这些本地代码的时候,实际上也是在使用编译阶段那些类和操作。在V8中,函数是一个基本单位。当某个js函数被调用时,属于该函数的本地代码就会生成。具体的工作方式是V8查找该函数是否已经生成本地代码,如果已经生成,直接调用。否则,V8会触发生成本地代码,目的是为了节约时间,减少去处理那些使用不到的代码的时间。

第二,运行时构建js对象,需要Runtime类来辅助,并需要从Heap类分配内存。

第三,获取对象属性,借助Runtime类中的辅助函数完成。

什么是优化回滚?

由于V8为了优化性能,引入了更为高效的 Crankshaft编译器。该编译器通常会做出比较乐观和大胆的预测,它会认为一些代码比较稳定,变量类型不会变化,所以能生成高效的本地代码。但是一旦某些变量类型改变了,就不得不使用一种机制来将它做的这些错误决定回滚到之前的一般情况,这个过程就是 优化回滚
这个操作非常不好,尽量避免。

什么是隐藏类?

V8使用 类和偏移位置**,将本来需要通过字符串匹配来查找属性值的算法改进为使用类似C++编译器的偏移位置的机制来实现,这就是 隐藏类(Hidden Class)
隐藏类将对象划分为不同的组,对于相同的组,也就是该组内对象拥有相同的属性名和属性值的情况,将这些属性名和对应的偏移位置保存在一个隐藏类中,组内所有对象共享该信息。同时,也可以识别属性不同的对象。

看代码和表格理解:

function ABC(x, y) {
  this.x = x;
  this.y = y;
}
var a = new ABC(1, 1);
var b = new ABC(2, 2);
隐藏类
属性名 偏移值
x 0
y 4

代码又是如何使用这些隐藏类来高效访问对象属性的呢(内嵌缓存)?

如果变量类型在很多情况下不变的话,那么会引入 内嵌缓存,它可以避免方法和属性被存取的时候出现的因哈希表查找而带来的问题。该机制的基本**是将使用之前查找的结果缓存起来,也就是说V8可以将之前查找的隐藏类和偏移值保存下来。

V8如何进行内存管理的?

分为以下两点来了解:

  1. 内存划分
  2. 垃圾回收机制

内存划分主要依赖于 Zone类 以及

首先看Zone类,它主要管理一系列的小块内存。如果用户想使用一系列的小内存,并且这些小内存的生命周期类似,这时可以使用一个Zone对象,这些小内存都是从Zone对象中申请的。

Zone对象先自己申请一款内存,然后管理和分配一些小内存。当一块小内存被分配后,不能被Zone回收,只能一次性回收Zone分配的所有小块内存。
例如,在构建抽象语法树之后,生成本地代码,然后抽象语法树的内存在这之后被一次性全部回收,效率非常高。
缺点就是,如果一个过程需要很多内存,那么Zone就需要分配大量的内存,但是又不能释放,会导致系统出现需要过多的内存而引起内存不够的情况。

其次看堆。V8使用堆来管理js使用的数据、生成的代码、哈希表等,为了更方便的实现垃圾回收,同很多虚拟机一样,V8将堆分成三个部分:

  1. 年轻分代:主要为新创建的对象分配内存,较容易回收。
  2. 年老分代:根据年老的对象、指针、代码等数据使用的内存,较少回收
  3. 为大对象保留的空间:为需要使用较多内存的大对象分配。每个页面只分配一个

什么是快照?

快照机制就是将内置对象和函数加载后的内存保存并序列化。

JavaScriptCore与V8有多少不同?

其实不同点主要在编译阶段,之前讲过,它会生成中间字节码,然后才会生成本地代码,而V8不需要,因为这个特点,所以又引入了一些其他机制来优化。

暂告结束

拓展

JavaScript 为什么快——第一篇?(共三篇)

读《webkit技术内幕》了解浏览器内核系列

  1. 读《webkit技术内幕》了解浏览器内核一
  2. 读《webkit技术内幕》了解浏览器内核二——内部架构
  3. 读《webkit技术内幕》了解浏览器内核三——资源与网络栈
  4. 读《webkit技术内幕》了解浏览器内核四——HTML、DOM以及CSS相关
  5. 读《webkit技术内幕》了解浏览器内核五——渲染基础
  6. 读《webkit技术内幕》了解浏览器内核六——硬件加速机制
  7. 读《webkit技术内幕》了解浏览器内核七——js引擎

string类型中的反斜杠以及正则无法匹配问题

起因

同事今早得到通知,需要对用户输入进行校验,不能输入一些特定的字符,其中包含\。同事在思考以及百度良久后,依然没找到解决方案,遂发到群里请大家一起帮忙,我处于好奇便试了下,发现里面还是有点内容的。

过程

chrome控制台直接输入测试

我一开始的测试用例:

/\\/.test('a\b') // false

是的,直接false,这我就很奇怪了,不是说单个\代表转义,需要两个\去匹配吗,怎么不行呢?

想不通,便去segmentfault上搜索,果然看到个答案:https://segmentfault.com/q/1010000005967122
按照链接所说,\不计入string的长度,我在浏览器中测试了下:

let str = 'a\c'
str.length // 2
str // 'ac'

???这好奇怪啊???我不理解,先是搜索了会,最终又去规范里面找了下,找到了解释:
http://www.ecma-international.org/ecma-262/5.1/#sec-15.10.4.1 或者 http://www.ecma-international.org/ecma-262/6.0/#sec-regexp-constructor 中的note那段话:

如果模式是StringLiteral.,则在由RegExp处理String之前执行通常的转义序列替换。如果模式必须包含要由RegExp识别的转义序列,则必须在StringLiteral内转义任何反斜杠字符,以防止在形成StringLiteral的内容时删除它们。

上面那段话理解起来就是StringLiteral模式下,包含转义字符的先尝试转义再匹配正则,无法转义的会将转义字符删掉!!!

这下就理解了,以前还真不知道呢。

input输入测试 2018-12-18补充

我依然对这个问题抱有一点疑问,如果用户真的在input中输入了a\之类的,怎么匹配???
以前貌似也没在意过这个问题啊。
不死心的我写了个简单的测试:

<!doctype>
<html lang="en">
<head>  
  <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"> 
  <title>input</title>
</head>
<body>  
<input type="text" id="input" oninput="listen(event)" onporpertychange="listen(event)">  
<script>    
let input = document.getElementById('input')    
let val    
function listen (e) {     
  console.log(e) 
  console.log(input.value) 
  val = input.value 
  console.log('val', val) 
  console.log('val type', typeof val) 
  console.log(val.length) 
  console.log(val.indexOf('\\'))    
  console.log(/\\/.test(val))    
}  
</script>
</body>
</html>

然后我在input中输入a\,天哪!!!没有任何错误且能正常匹配!!!
what???
怎么回事???

接下来几乎一天都耗在这个问题上了。。。
主要一直在查规范,奈何本人英语较挫,勉强看看。

先看:https://www.w3.org/TR/html5/sec-forms.html#the-input-element 这个链接,规范中关于input元素的内容。
在上面链接中我注意到value属性标注的是DOMString类型,以前也有看过,以为就是string所以没在意,这次得去查查了。
然后找到规范中关于DOMString的内容:https://heycam.github.io/webidl/#es-DOMString
最后又去MDN上找了下:https://developer.mozilla.org/zh-CN/docs/Web/API/DOMString
就说了这么一段话:
DOMString 是一个UTF-16字符串。由于JavaScript已经使用了这样的字符串,所以DOMString 直接映射到 一个String。

总结

我怀疑可能就是因为这个DOMString映射为String时,将用户输入的单个\给转义了,因为在我的测试中,/\\/.test()是能够匹配用户输入的 a\ 中的斜杠的,与直接在浏览器控制台写string字面量有所区别。
1.input输入时,/\\/.test()是能够匹配输入的单个''的;
2.直接在浏览器控制台输入string字符串时,以单个''结尾直接报错,这里应该是StringLiteral模式了,所以符合规范的定义。

读《JavaScript设计模式与开发实践》学习 职责链 模式

啰嗦

  在家蹉跎一段时间,终于找到工作了,扬州真互联网荒漠,不过幸运的是居然在隔壁镇上找到java开发工作,虽然技术很老旧,我都不会的那种,好在同事之间互帮互助。就这样先在农村蛰伏,养好身体,当然,技术这里不能荒废,还是要学的。
  目前最大的问题就是学习效率极度低下。比如《es6入门》,前后看了不下5遍,常用的不看都能记住,不怎么用的比如generator还有async/await等较新的api总是忘记怎么玩,太菜了。不过每次看都有收获吧。
  《JavaScript设计模式与开发实践》这书也有整一个月没看了,今天拾起来继续攻读,收获颇丰,这个职责链模式真的让我这个代码质量极差的人耳目一新!

搬运工

  书上写的很棒了,我简要讲下,并搬运过来,主要警示我以后写业务代码的时候,多动点脑子,写出那种赏心悦目、维护性高的代码。
  职责链模式其实有点链式传递的意思,在js中,比如作用域链、原型链都有它的身影。简要概括就是:一个请求有多个对象来处理,这些对象是一条链,但具体由哪个对象来处理,根据条件判断来确定,如果不能处理会传递给该链中的下一个对象,直到有对象处理它为止。责任链模式将请求和处理分离开来,进行解耦。

考虑下面场景:电商平台做活动,比如付定金预购某商品得到优惠券,500元定金可以得到100优惠券,200元定金可以得到50元优惠券,优惠券直接当钱用,而不买优惠券就原价购买商品,原价购买商品受库存影响。这是个十足的业务需求,我很可能会写出类似书上的那种许多if...else的糟糕代码,然后被队友疯狂吐槽:

// orderType: 订单类型;1--花500买的优惠券;2--花200买的优惠券;3--原价购买
// stock:库存剩余数量,已买优惠券的不受此限制
if (orderType === 1) {
  console.log('付了500元定金,得到100优惠券')
}
if (orderType === 2) {
  console.log('付了200元定金,得到50优惠券')
}
if (orderType === 3 && stock > 0) {
  console.log('原价购买')
}

咋一看,好像3个if判断没问题,业务的确解决了,但是电商业务是经常变化的!我这里直接考虑的是用户下单付了定金的情况,如果像书上那样存在下单不付钱的情况呢?
我是不是要继续加判断?
如果优惠券是限量的,用户下单但没付款呢,我是不是还得加判断?
随着判断越来越多,这段代码将变得非常难看,甚至直接影响心情!
那我该怎么办?

利用职责链模式来重构代码!
第一版

var order500 = function(orderType, stock) {
  if (orderType === 1) {
    console.log('付了500元定金,得到100优惠券')
  } else {
    order200(orderType, stock)
  }
}

var order200 = function(orderType, stock) {
  if (orderType === 2) {
    console.log('付了200元定金,得到50优惠券')
  } else {
    orderNormal(orderType, stock)
  }
}

var orderNormal = function(orderType, stock) {
  if (stock > 0) {
    console.log('普通购买,无优惠券')
  } else {
    console.log('库存不足')
  }
}

第一版代码将每种情况封装成函数,根据业务区调用相应的函数即可,即使特定业务条件修改,也只需要修改函数内部判断,比之前一堆if连在一起好看得多。
不过,这种方法也存在缺陷,用书上的话讲,传递请求的代码被耦合在了业务函数中,违反了开放——封闭原则。如果以后增加300元预定或者去掉200元预定,也需要改动较多。那么这条职责链会变得非常不稳固!

如何改进?看看第二版:

var order500 = function(orderType, stock) {
  if (orderType === 1) {
    console.log('付了500元定金,得到100优惠券')
  } else {
    return 'nextSuccessor'
  }
}

var order200 = function(orderType, stock) {
  if (orderType === 2) {
    console.log('付了200元定金,得到50优惠券')
  } else {
    return 'nextSuccessor'
  }
}

var orderNormal = function(orderType, stock) {
  if (stock > 0) {
    console.log('普通购买,无优惠券')
  } else {
    console.log('库存不足')
  }
}

var Chain = function (fn) {
  this.fn = fn;
  this.successor = null;
}

Chain.prototype.setNextSuccessor = function (successor) {
  this.successor = successor
}

Chain.prototype.passRequest = function () {
  var ret = this.fn.apply(this, arguments);
  if (ret === 'nextSuccessor') {
    return this.successor && this.successor.passRequest.apply(this.successor, arguments)
  }
  return ret
}
// 分别包装成职责链节点
var chainOrder500 = new Chain(order500);
var chainOrder200 = new Chain(order200);
var chainOrderNormal = new Chain(orderNormal);
// 指定职责链顺序
chainOrder500.setNextSuccessor(chainOrder200);
chainOrder200.setNextSuccessor(chainOrderNormal);
// 传递请求
chainOrder500.passRequest(1, 500); // 付了500元定金,得到100优惠券
chainOrder500.passRequest(2, 500); // 付了200元定金,得到50优惠券
chainOrder500.passRequest(3, 500); // 普通购买,无优惠券
chainOrder500.passRequest(3, 0); // 库存不足

通过改进,我们可以自由灵活地增加、移除和修改链中的节点顺序,可维护性更高!

以上代码来自书中,我仅仅将其搬运过来,方便日常查找。

职责链模式也存在一定的弊端,首先不能保证请求一定会被链中节点处理,其次过长的职责链也会带来性能损耗。

回味

其实对于这个模式,我业务中还没有实践,所以只停留在理论上,不过它很适合已知参数但是未知参数值的if...else判断

读《webkit技术内幕》了解浏览器内核一

前言

当前端开发一段时间后,接触到的知识越来越多,同时带来的疑问也会越来越多。

比如 事件循环,我看过不少相关文章,讲的有好有坏,但是都会提到 主线程 等名词,当时我就很好奇,这个主线程是什么?(我不是科班出身,对这个还真是一窍不通)

我还看到 js是单线程的 这类知识,那么是不是浏览器就只有一个主线程?

而网页一般除了js文件,肯定还有css和html文件,它们也是和js一样用主线程处理的嘛?
...

以上很多问题,在一年多前真的困扰着我。

浏览器对我而言就是一个黑盒,我只会用点皮毛,我对里面的运行机制很困惑(并不是求知欲,只是非科班出身的我感到惶恐不安,感到自己真的菜,一种焦虑吧),当时我不断去搜索相关知识,终于找到篇不错的博客,是一位阿里大佬写的,当时我看完真的是豁然开朗,虽然我不知道他讲的对不对,但是他的博客确实让我学到了东西,奉上他博客链接:

从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理

虽然我当时真的认认真真看完那篇博客,但还是有很多知识点我不懂,不熟。

也是在一年前,我偶然发现了这本《webkit技术内幕》,我立马淘宝下单买了,然后吃灰一年,今年回老家,时间较为充足,然后只用了一周将这本书大致翻了一遍,如果你问我记住什么,我会说很多没记住,不是书不好,只是个人学习能力问题。

所以,特地开issue,靠着上周翻书及所画的一些知识点,记录下来,方便自己对webkit内核运行机制理解。

这本书,是作者研究了webkit项目源码,以及结合浏览器实际表现所总结的,非常值得一看。书中涉及到很多源码的类名,这些不需要死记硬背,只需要大致了解进程之间如何通信,线程之间如何通信,是不是所有资源都在同一进程中执行等等问题。

接下来,我会以问题的方式来记录。

1. webkit是什么?

webkit是由美国苹果公司开源的项目,它是Safari浏览器的内核!

2. 浏览器内核又是什么?

在浏览器中,有一个最重要的模块,它主要的作用是将页面转变成可视化(准确讲还要加上可听化)的图像结果,这就是浏览器内核。通常,它也被称为渲染引擎

3. 我们或许听过谷歌浏览器/nodejs的v8引擎,它是渲染引擎嘛?它和webkit是什么关系?

v8不是渲染引擎,v8是js引擎,它是用来编译和执行s代码的!!!

4. 是不是所有浏览器的渲染引擎都用webkit内核?js引擎都用v8引擎?

不是。

4.1 Safari浏览器

webkit内核一开始是苹果公司Safari浏览器在用。
相应的js引擎是JavaScriptCore

4.2 谷歌旗下的浏览器

js引擎是v8

google以webkit为基础,创建了一个新项目:Chromium,该项目的目标是创建一个快速的、支持众多操作系统的浏览器。

google又在Chromium基础上,开发了Chrome浏览器。

后来,谷歌又在webkit基础上进行改版,创建了Blink内核,目前Chrome和Chromium用的都是Blink内核

注意:Chrome和Chromium都是浏览器,Chromium可以看作是Chrome的先行版(开源试验场),Chromium会尝试很多创新并且大胆的技术,当这些技术稳定之后,Chrome才会把它们集成进来。

4.3 微软旗下的浏览器

js引擎是Jscript和Chakra(IE9及以上)

window装机必备的IE浏览器用的是微软自研的Trident内核。不过,微软已经停止对IE的迭代更新了,开发了新的浏览器 Edge,使用的也是新研发的 EdgeHTML内核,不过,微软也宣布将会换成Chromium/Blink内核

4.4 firefox浏览器

火狐使用的是自研的Gecko内核SpiderMonkey js引擎

4.5 国内的浏览器

由于历史原因,国内很多浏览器一开始用的是IE Trident内核,后来,很多浏览器又开始集成其他类似webkit等内核,也就是双核浏览器。

5. 内核内部是什么样的?它包含哪些内容?

看图列:

虚线框住的是渲染引擎提供的功能

6. Blink内核和webkit内核有什么区别吗?

区别还是不少的,Google希望未来在Blink中加入很多新技术,列出书中提到的:

  1. 实现跨进程的iframe。为iframe创建一个单独的沙箱进程。
  2. 重新整理和修改WebKit关于网络方面的架构和接口。
  3. 更为胆大更为激进的想法就是将DOM树引入JavaScript引擎中。
  4. 针对各种技术的性能优化,包括但是不限于图形、JavaScript引擎、内存使用、编译的二进制文件大小等。

7.webkit如何渲染网页?

7.1 加载和渲染

按照一些文档分析,包含两个过程:

  1. 网页加载过程,就是从URL到构建DOM树;
  2. 网页渲染过程,从DOM树到生成可视化图像。

这两个过程会交叉,很难给与明确的区分,所以统称为网页的渲染过程。

7.2 webkit渲染过程

较为完整的图例:

书中给出的图例

具体过程如下:

  1. 用户输入URL,webkit调用其资源加载器加载该URL对应的网页;
  2. 加载器依赖网络模块建立连接,发送请求并接收答复;
  3. webkit接收到各种网页或者资源的数据,其中某些资源可能是同步或异步获取的;
  4. 网页被交给HTML解释器转变成一系列的词语(Token);
  5. 解释器根据词语构建节点(Node),形成DOM树;
  6. 如果节点是js代码,调用js引擎解释并执行;
  7. js代码可能会修改DOM树结构
  8. 如果节点依赖其他资源,例如图片、CSS、视频等,调用资源加载器来加载它们,但是它们是异步的,不会阻碍当前DOM树的继续创建;如果是js资源URL(没有标记异步方式),则需要停止当前DOM树的创建,直到js的资源加载并被js引擎执行后才继续DOM树的创建。

7.3 了解渲染过程后,你知道DOMContentLoaded和onload有区别吗?

有区别!

看渲染过程以及从变量名就该知道,DOMContentLoaded发生在DOM树构建成功后,请注意,DOM树此时并没有和style结合生成Render树,而onload官方说明是网页所有资源加载完成后才会触发,而网页上会有很多资源是异步加载的,异步加载的资源不会阻碍DOM树的构建,所以onload一般在DOMContentLoaded后面执行。

DOMContentLoad和onload触发时间图例:

7.4 从渲染过程图可知,DOM树会和Style树结合成Render树,然后才会绘制最终展示,那么它们如何结合的呢?

简单点讲分为下面步骤:

  1. CSS文件被CSS解释器解释成内部表示结构;
  2. CSS解释器工作完之后,在DOM树上附加解释后的样式信息,这就是RenderObject树;
  3. RenderObject节点在创建的同时,webkit会根据网页的层次结构创建RenderLayer树,同时构建一个虚拟的绘图上下文。

这里附上书中的图例:

RenderObject树的建立并不表示DOM树会被销毁,事实上,上述图中的四个内部表示结构一直存在,直到网页被销毁,因为它们对于网页的渲染起了非常大的作用。

读《webkit技术内幕》了解浏览器内核系列

  1. 读《webkit技术内幕》了解浏览器内核一
  2. 读《webkit技术内幕》了解浏览器内核二——内部架构
  3. 读《webkit技术内幕》了解浏览器内核三——资源与网络栈
  4. 读《webkit技术内幕》了解浏览器内核四——HTML、DOM以及CSS相关
  5. 读《webkit技术内幕》了解浏览器内核五——渲染基础
  6. 读《webkit技术内幕》了解浏览器内核六——硬件加速机制
  7. 读《webkit技术内幕》了解浏览器内核七——js引擎

从vue源码中学习数据代理

起因

最近项目不忙,就自己继续跟着那位滴滴前辈的vue源码分析去看vue源码,不得不说,滴滴前辈真的厉害!今天看到https://ustbhuangyi.github.io/vue-analysis/reactive/reactive-object.html#proxy 这里时,没有第一时间理解,然后自己在控制台尝试去输出才恍然大悟,自己还是菜。。。

利用Object.defineProperty去实现数据代理

这里对应vue源码里的 instance/state.js 中的proxy函数,根据滴滴前辈的分析,这里通过调用proxy函数把每一个值 vm._data.xxx 都代理到 vm.xxx 上;

我一开始看了下代码,根据我的理解,读写vm.xxx时的确是读写vm._data.xxx,但是读写vm._data.xxx的同时也会代理到vm.xxx上我就不理解了,既然光看代码不理解,咱就动手吧。

在chrome控制台测试:

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: function () {},
  set: function () {}
}

function proxy (target, sourceKey, key) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

let obj = {
  a: {
    a1: 1
  }
}
proxy(obj, 'a', 'a1')
obj // {a: {a1: 1}, a1: 1}
obj.a.a1 = 2
obj // {a: {a1: 2}, a1: 2}

why???amazing!!!

我仔细想了想,a1a都是obj的属性,当打印obj时其实就已经在调用它各个属性的getter方法来获取值了,而proxy函数正好改变了obj.a1settergetter方法!

Proxy

vue源码里也用到了Proxy这个新api,其实我以前看《es6入门》时就没大看懂,今天特地重温了下,豁然开朗。。。不懂得继续看阮一峰前辈的书吧。。。es6入门--Proxy

读《数据结构与算法 JavaScript描述》——实现一个队列结构

什么是队列?

队列是一种列表,队列只能在队尾插入元素,在队首删除元素。队列用于存储按顺序排列的数据,先进先出
可以把队列想象成一条从左向右运行的传输带,只能在传输带的左侧放东西,在传输带的右侧取出。

如何实现队列?

实现队列前,先要确认该用什么底层结构去实现?
鉴于队列的特性,这里依然采用数组去实现。

队列包含哪些方法/属性?

  1. 由于队列只能在队尾插入(入队),队首删除(出队),对应数组的pushshift方法。
  2. 队列也可以读取队首元素,对应数组第一个元素。
  3. 队列也可以清空,对应数组清空。
  4. 队列也可以查看含有多少元素,对应数组长度。

有了以上四点分析,我就可以开始实现了:

function fakeQueue () {
  this.data = []
}

fakeQueue.prototype.push = function (ele) {
  this.data.push(ele)
}

fakeQueue.prototype.shift = function () {
  this.data.shift()
}

fakeQueue.prototype.clear = function () {
  this.data.length = 0
}

fakeQueue.prototype.peek = function () {
  return this.data[0]
}

fakeQueue.prototype.length = function () {
  let len = this.data.length
  return this.data[len - 1]
}

// 测试
var q = new fakeQueue()
q.push(1)
q.peek() // 1
q.length() // 1
q.push(2,3,4)
q.length() // 2
q.clear()
q.length() // 0

如上,最简单的队列结构就实现了。

基于react+ts写了个级联选择器——react-cascader-transfer

npm地址:https://www.npmjs.com/package/react-cascader-transfer

github:https://github.com/lizhongzhen11/react-cascader-transfer

效果图:

开发背景

最近做的项目中涉及到地区选中以及项目及项目下的设备选择等需求。采用的是react + ts + antd开发。

antd很不错,提供了级联选择器,只是,它把级联放到下拉框里我不喜欢,而且只能单选,一开始有想把 cascader 和 transfer 结合下来解决我需要的,就像官方提供的 tree + transfer 那样,但是实际操作起来并不容易,最主要的是,我并不需要transfer,这个transfer 的效果不是我想要的。

想起来,上家公司工作时,产品经理抄头条广告后台管理系统的UI时,不仅抄了星期时间范围选择器,还抄了它的级联交互,不过那个是我同事写的,基于vue写的,具体代码是什么我也没仔细看过,依稀记得他说用了什么算法的,哎,时间紧当初没好好看,自己现在要用到类似的组件了,自己只能从头写了。

开发过程

完成 vue-week-time-range-pickerreact-week-time-range-picker 后我就开始想要去开发这个 react-cascader-transfer 组件了。
嗯,当时是10月4号还是5号来着的,我上班了,然后,我搭了个环境,然后,神游太虚。

第二天,我画了个已选面板,调下样式,神游太虚。

第三天,我开始画级联面板,调调样式,神游太虚。

第四天,开始。

总的来说,还是有点复杂的,我代码写的也不算好,用到了不少递归。

首先这种级联吧,得确定多少层级,那么肯定是常见的 tree 结构,为了给组件用,肯定需要统一 tree 的格式,我看了下antd的 Cascader Option,嗯,就用它了,在它基础上扩展下就好了。

结构有了,要考虑如何展开子层?
初始化就去确定有多少层还是手动点击在确定展开的层级呢?

我选择第二种手动点开确定。初始化就展开没啥必要。

这就需要考虑展开的层级数据,仅仅靠之前的 tree 结构不足以完成,我因此扩展了 level 属性,确定层级。
同时,我还需要确定父子关系,仅靠 children 属性,子层无法得知谁是父层,因此我还要添加个 parentId 属性来确定。(PS:有的数据可能会提供该属性,有的话直接用就可以了,否则我会默认让父层的 value 成为子层的 parentId

展开依赖于点击事件,这里就要注意到:可能子层已经展开过了,这个情况要避免。

这个完成后,神游太虚。

第五天,去了趟南京复查。

第六天,要加checkbox和checkbox选中状态改变了。
为了实现这个功能,需要加 checked 属性,顺带说一下,checkbox用的 antd 现成的,所以打包起来有点大。。。

checkbox选中状态改变,子孙层肯定会随着改变,但是父层不一定。子孙层的checkbox状态跟着当前checkbox改变层的状态而变化。

但是,需要考虑到以下两种情况:

  1. 当前层取消选中,其父层原先选中,需要将其父层也取消选中;
  2. 当前层选中,其兄弟层都已经选中,需要将父层也选中。

针对这两种情况,主要逻辑代码在 changeAncestorsChecked 方法中,会一直递归到顶层。

思考+编写测试一段时间,完成了。

然后就是具体的数据问题了,不管怎么点击,我都需要将数据实时拿到,会涉及到子层数据全选,合并成父层数据来替换,这些步骤花了不少时间,主要逻辑代码依然在 changeAncestorsCheckedhandleSelected 方法中。

当然,由于判断太多,使用了策略模式优化了部分代码,也算学到的东西有了用武之地。
不过开发到下班时,还有bug。

今天第七天,早上奋力敲代码,总算完成初版,包括删除右侧已选中面板数据。

目前已发到npm上,地址:https://www.npmjs.com/package/react-cascader-transfer

感想

写这些组件一是为了方便以后自己用,二是寻求点成就感。
如果我不写,
那么我不会知道如何发包到npm上;
我也不会去想着用webpack搭建简单的react或vue环境;
我更不会将之前所学的知识用于实践;
最终,碰到点复杂的东西,我可能都不愿意去动脑子思考;
久而久之,我可能就固步自封了。

但是我写了,有了新的尝试,新的收获。

Object.prototype.toString.call()

记得之前在某篇讲解vue源码的文章里第一次看到这个方法,心里便留意了起来,后来日常开发需要去判断类型时百试不爽,但一直不明所以,直到今天在看了如何继承Date对象?由一道题彻底弄懂JS继承。这篇博客,我才大致了解到原来该方法访问的是对象的[[Class]]属性。

[[Class]]与Internal slot

  • 在ES5中,每种内置对象都定义了 [[Class]] 内部属性的值,[[Class]] 内部属性的值用于内部区分对象的种类
    • Object.prototype.toString访问的就是这个[[Class]]
    • 规范中除了通过Object.prototype.toString,没有提供任何手段使程序访问此值。
    • 而且Object.prototype.toString输出无法被修改
  • 而在ES6中,之前的 [[Class]] 不再使用,取而代之的是一系列的internal slot
    • Internal slot 对应于与对象相关联并由各种ECMAScript规范算法使用的内部状态,它们没有对象属性,也不能被继承
    • 根据具体的 Internal slot 规范,这种状态可以由任何ECMAScript语言类型或特定ECMAScript规范类型值的值组成
    • 通过Object.prototype.toString,仍然可以输出Internal slot
    • 简单点理解(简化理解),Object.prototype.toString的流程是:如果是基本数据类型(除去Object以外的几大类型),则返回原本的slot, 如果是Object类型(包括内置对象以及自己写的对象),则调用Symbol.toStringTag
    • Symbol.toStringTag方法的默认实现就是返回对象的Internal slot,这个方法可以被重写

ES6新增的一些,这里未提到:(如Promise对象可以输出[object Promise]

读《数据结构与算法 JavaScript描述》——实现一个链表结构

什么是链表?

  可以看:https://www.zhihu.com/topic/19649942/intro

  链表(Linked list)是一种常见的基础数据结构,是一种线性表,是一种物理存储单元上非连续、非顺序的存储结构。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括存储数据元素的数据域存储下一个结点地址的指针域两个部分。 相比于线性表顺序结构,操作复杂。数据元素的逻辑顺序也是通过链表中的指针链接次序实现的。

  链表分为:单(向)链表、循环链表、双向链表。虽然有三种不同的链表,但是其中心**(存储的逻辑结构)是一样的。

从网上三张图来分别展示下

单向链表:

双向链表:

循环链表:

如何实现?

单向链表为例:

  1. 每个节点要有两个属性data和next,data存放数据,next指向下一个节点;
  2. 最后一个节点指向null
  3. 需要有一个头结点

如此,我可以先实现一个节点类:

function Node (ele)  {
  this.data = ele;
  this.next = null;
}

如何将节点连接起来构成链表呢?
这时候我需要有一个类,用于维护链表的插入、删除等方法。当然,需要先给它一个头节点。

function LinkedList () {
  this.head = new Node('head');
}

头节点有了,接下来如何去插入新节点使其连接呢?
这时候的头节点的next指向的是null,我需要在插入方法中去改变next指向。
我肯定是要插入Node节点对象的,同时头节点的next要指向这个新节点,那么初步实现下:

LinkedList.prototype.insert = function (ele) {
  this.head.next = new Node(ele)
}

但是,问题来了!
我这个insert里面只改变了头节点next指向,如果我想插入第二个甚至更多节点,目前的方法根本不满足!!!所以,我需要记住每个插入的节点。
并且,链表是可以不按顺序插入的,那我必须得知道新节点要插到哪个位置(哪个节点后面),所以我必须找到该节点。
那么插入新节点时,不仅要将新节点数据传入,同时也要将需要插入新节点的前一个节点数据传入才行。

改写下insert:

LinkedList.prototype.insert = function (ele, prev) {
  var curr = this.head
  while (curr.next && curr.data !== prev) {
    curr = curr.next
  }
  if (curr.data !== prev) {
    throw new Error('请在已有节点后插入')
  }
  var newNode = new Node(ele)
  newNode.next = curr.next
  curr.next = newNode
}

// 测试下
var l = new LinkedList()
l // LinkedList{head: {data: 'head', next: null}}
l.insert(1, 1) // Error
l.insert('first', 'head')

接下来,需要实现删除操作。删除的话需要知道被删除节点的前后节点才行,那么首先需要去找前一个节点,附上代码:

LinkedList.prototype.remove = function (item) {
  var curr = this.head
  while (curr.next && curr.next.data !== item) {
    curr = curr.next
  }
  if (!curr.next && curr.data !== item) {
    throw new Error('请删除已有节点')
  }
  var removeNode = curr.next
  curr.next = removeNode.next 
}

// 测试下
var l = new LinkedList()
l // LinkedList{head: {data: 'head', next: null}}
l.insert('first', 'head')
l.remove('first')
l.remove('first') // Error

基本上单向链表算是实现了,最终代码:

function Node (ele)  {
  this.data = ele;
  this.next = null;
}

function LinkedList () {
  this.head = new Node('head');
}

LinkedList.prototype.insert = function (ele, prev) {
  var curr = this.head
  while (curr.next && curr.data !== prev) {
    curr = curr.next
  }
  if (curr.data !== prev) {
    throw new Error('请在已有节点后插入')
  }
  var newNode = new Node(ele)
  newNode.next = curr.next
  curr.next = newNode
}

LinkedList.prototype.remove = function (item) {
  var curr = this.head
  while (curr.next && curr.next.data !== item) {
    curr = curr.next
  }
  if (!curr.next && curr.data !== item) {
    throw new Error('请删除已有节点')
  }
  var removeNode = curr.next
  curr.next = removeNode.next 
}

双向链表其实就是在单向链表基础上加一个指向前节点的链接,直接放书上的代码吧:

function Node (element) {
  this.element = element
  this.next = null
  this.previous = null
}

function LList () {
  this.head = new Node('head')
  this.find = find
  this.insert = insert
  this.display = dispay
  this.remove = remove
  this.findLast = findLast
  this.dispReverse = dispReverse
}

function dispReverse () {
  var cuuNode = this.head
  currNode = this.findLast()
  while (!(currNode.previous === null)) {
    currNode = currNode.previous
  }
}

function findLast () {
  var currNode = this.head
  while (!(currNode.next === null)) {
    currNode = currNode.next
  }
  return currNode
}

function remove () {
  var currNode = this.head
  if (!(currNode.next === null)) {
    currNode.previous.next = currNode.next
    currNode.next.previous = currNode.previous
    currNode.next = null
    currNode.previous = null
  }
}

function display () {
  var currNode = this.head
  while (!(currNode.next === null)) {
    currNode = currNode.next
  }
}

function find (item) {
  var currNode = this.head
  while (currNode.element !== item) {
    currNode = currNode.next
  }
  return currNode
}

function insert (newElement, item) {
  var newNode = new Node(newElement)
  var current = this.find(item)
  newNode.next = current.next
  newNode.previous = current
  current.next = newNode
}

循环链表和单向链表相似,节点类型都是一样的。唯一的区别是,在创建循环链表时,让其头节点的next属性指向它本身,即:

head.next = head

这种行为会传导至链表的每个节点,使得每个节点的next属性都指向链表的头节点。换句话说,链表的尾节点指向头节点,形成一个循环链表

参考

https://zhuanlan.zhihu.com/p/29627391
http://c.biancheng.net/view/3336.html

读《webkit技术内幕》了解浏览器内核四——HTML、DOM以及CSS相关

前言

本篇继续跟着书学习HTML解释器、DOM模型以及CSS解释器相关知识,书中这是两大章,不过由于我之前大致看过,划过一些点,所以本篇打算将它们融合。

资源最初的表示就是字节流,《深入理解计算机系统》也提到,所有的系统信息都是由一连串的位表示,而 1字节(byte) = 8位(bit)

重要问题:那么从网络/本地文件获取的字节流是如何转成WebKit内部表示结构——DOM树的呢?

要回答这个大问题,需要去了解HTML解释器和DOM模型以及WebKit内部的处理,接下来还是分成若干个小问题去逐步了解。

什么是DOM模型?

DOM(Document Object Model)全称 文档对象模型,它可以以一种 独立于平台和语言 的方式访问和修改一个文档的内容和结构。

DOM定义的是一组与平台、语言无关的 接口。该接口允许编程语言动态访问和更改结构化文档。使用DOM表示的文档被描述成一个 树形结构

DOM树的结构模型长什么样?或者说如何构成?

DOM结构构成的基本要素是 节点,而文档的DOM结构就是由层次化的节点组成。在DOM模型中,整个文档(Document)就是一个节点,称为文档节点。HTML中的标记(Tag)也是一种节点,称为元素(Element)节点。还有一些其他类型节点,例如属性节点、Entity节点、ProcessingIntruction节点、CDataSection节点、注释(Comment)节点等。

DOM标准看:https://dom.spec.whatwg.org/ ,看看Document接口吧(PS:使用IDL语言描述):

[Constructor,
 Exposed=Window]
interface Document : Node {
  [SameObject] readonly attribute DOMImplementation implementation;
  readonly attribute USVString URL;
  readonly attribute USVString documentURI;
  readonly attribute USVString origin;
  readonly attribute DOMString compatMode;
  readonly attribute DOMString characterSet;
  readonly attribute DOMString charset; // historical alias of .characterSet
  readonly attribute DOMString inputEncoding; // historical alias of .characterSet
  readonly attribute DOMString contentType;

  readonly attribute DocumentType? doctype;
  readonly attribute Element? documentElement;
  HTMLCollection getElementsByTagName(DOMString qualifiedName);
  HTMLCollection getElementsByTagNameNS(DOMString? namespace, DOMString localName);
  HTMLCollection getElementsByClassName(DOMString classNames);

  [CEReactions, NewObject] Element createElement(DOMString localName, optional (DOMString or ElementCreationOptions) options = {});
  [CEReactions, NewObject] Element createElementNS(DOMString? namespace, DOMString qualifiedName, optional (DOMString or ElementCreationOptions) options = {});
  [NewObject] DocumentFragment createDocumentFragment();
  [NewObject] Text createTextNode(DOMString data);
  [NewObject] CDATASection createCDATASection(DOMString data);
  [NewObject] Comment createComment(DOMString data);
  [NewObject] ProcessingInstruction createProcessingInstruction(DOMString target, DOMString data);

  [CEReactions, NewObject] Node importNode(Node node, optional boolean deep = false);
  [CEReactions] Node adoptNode(Node node);

  [NewObject] Attr createAttribute(DOMString localName);
  [NewObject] Attr createAttributeNS(DOMString? namespace, DOMString qualifiedName);

  [NewObject] Event createEvent(DOMString interface);

  [NewObject] Range createRange();

  // NodeFilter.SHOW_ALL = 0xFFFFFFFF
  [NewObject] NodeIterator createNodeIterator(Node root, optional unsigned long whatToShow = 0xFFFFFFFF, optional NodeFilter? filter = null);
  [NewObject] TreeWalker createTreeWalker(Node root, optional unsigned long whatToShow = 0xFFFFFFFF, optional NodeFilter? filter = null);
};

[Exposed=Window]
interface XMLDocument : Document {};

dictionary ElementCreationOptions {
  DOMString is;
};

依靠什么才能将字节流转成DOM树呢?

HTML解释器

大致过程可以看图:

如上图,什么是词法分析?

在进行此法分析之前,解释器首先要做的事情就是检查该网页内容使用的编码格式,以便后面使用合适的解码器。如果解释器在HTML中找到了设置的编码格式,WebKit会使用相应的解码器来将字节流转换成特定格式的字符串。如果没有特殊的格式,词法分析器 HTMLTokenizer类 可以直接进行此法分析。

简单来说,HTMLTokenizer类 就是一个状态机,输入字符串,输出词语。

主要接口是 nextToken函数,循环执行。大致流程看图:

词语Token如何构建成节点呢?

WebKit中会使用 HTMLDocumentParser 类调用 HTMLTreeBuilder 类的 constructTree 函数来实现的。该函数内部会调用 processToken函数。

节点如何构建DOM树呢?

WebKit会使用 HTMLConstructionSite 类来完成。内部维护一个保存元素节点的栈,其中的元素节点是当前有开始标记但是还没有结束标记的元素节点(HTML文档的Tag标签是有开始和结束标记的)。

也就是说,当元素节点出现结束标记的话,会退栈。这里用栈表示真的很好,充分利用栈的后进先出特点。例如:

<html lang="en">
<head>
<meta charset="utf-8" />
<title>Test</title>
</head>
<body>
</body>
</html>

以上述HTML代码为例,<html>根节点最先入栈,然后是<head>,之后是<meta />,发现<meta />有结束标记,直接出栈,依次类推。

之前介绍过浏览器的进程和线程,那么解析HTML操作在哪个进程里面呢?

以Chromium为例,在Renderer进程中有一个线程专门处理HTML文档解释任务。

网络资源的字节流自IO线程传递给 渲染线程 后,之后的解释、布局和渲染等工作基本上都是在该线程。因为DOM树只能在渲染线程上创建和访问,但是,从字符串到词语这个阶段可以交给单独的线程来做。

刚开始学前端时,我们大都知道不要在head标签里面写js,尤其不要在里面尝试获取DOM,这是为什么呢?

结合上述问题可以得知,如果在<head></head>里面写js并尝试获取DOM,下面的DOM节点可能还没出栈创建节点呢,当然获取不到了。

DOM还提供了非常重要的事件处理机制,它是如何工作的?

事件在工作过程中使用两个主体,第一个是事件,第二个是事件目标。事件处理最重要的部分就是事件捕获和事件冒泡。

当渲染引擎接收到一个事件时,它会通过 HitTest 算法检查哪个元素是直接的事件目标,如果发现有监听者,它会将这些事件传给WebKit,WebKit最后调用js引擎来触发监听者函数。但是,浏览器可能也会根据这些事件仍然处理它的默认行为,这会导致竞争冲突,所有Web开发者在监听者的代码中应该调用该事件的 preventDefault 函数来阻止浏览器触发它的默认处理行为。

我们通过浏览器可以查看网页DOM结构,但是,当使用

Shadow(影子) DOM

浏览器调试时会发现,<video>标签下会包裹着 shadow-root字样,看图

什么是Shadow DOM?

Shadow DOM能够使一些DOM节点在特定范围内可见,而在网页的DOM树中不可见,但是网页渲染结果中包含了这些节点。有点封装成DOM组件的意思。

怎么处理Shadow DOM上的事件?

由于内部节点不可见,那么事件目标其实就是包含Shadow DOM子树的节点对象。其他的不变。

了解了HTML加载解析过程后,CSS的解析又是什么情况呢?

渲染过程的什么时间段会进行CSS的解析呢?

从整个网页的加载和渲染过程来看,CSS解释器和规则匹配处于DOM树建立之后,RenderObject树建立之前,CSS解释器解释后的结果会保存起来,然后RenderObject树基于该结果来进行规范匹配和布局计算。

能大概说说CSS的解释过程吗?

首先,CSS解释过程是指从CSS字符串经过CSS解释器处理后变成渲染引擎的内部规则表示的过程。

由于涉及到内部类,看图了解下即可:

CSS经过上图解释后变成了样式规则,可是样式规则如何与具体的HTML进行匹配结合呢?

样式规则建立完成之后,WebKit保存规则结果在 DocumentRuleSets 对象类中。

由于涉及到内部类,依然看图了解下即可:

根据实际需求,每个元素可能需要匹配不同来源的规则,依次是用户代理(浏览器)规则集合、用户规则集合和HTML网页中包含的自定义规则集合。三个规则匹配方式是类似的。

在日常开发中,我有用过js去动态的改变CSS样式,这是如何做到的呢?

通过CSSOM(CSS对象模型)。

它的**是在DOM中的一些节点接口中,加入获取和操作CSS属性或者接口的JavaScript接口,因而js可以动态操作CSS样式。

HTML以及CSS解析结合之后,又是如何布局呢?

当WebKit创建RenderObject对象之后,每个对象是不知道自己的位置、大小等信息的,WebKit根据盒模型来计算它们的位置、大小等信息的过程称为布局计算(排版)。

布局计算根据其计算的范围大致可以分为两类:

  1. 第一类是对整个RenderObject树进行的计算;
  2. 第二类是对RenderObject树中某个子树的计算,常见于文本元素或者是overflow: auto;块的计算,这种情况一般是其子树布局的改变不会影响其周围元素的布局,因而不需要重新计算更大范围的布局。

布局计算的具体过程是什么呢?或者说是怎么进行的?

布局计算是一个 递归 的过程,因为一个节点的大小通常需要先计算它的子女节点位置、大小等信息。

看图理解下:

主要通过RenderObject类的 layout函数来完成 。

  1. 首先,判断RenderObject节点是否需要重新计算,通常需要检查位数组中的相应标记位、子女是否需要计算布局等来确定;
  2. 其次,该函数会确定网页的宽度和垂直方向上的外边距,这是因为网页通常是在垂直方向上滚动,而水平方向尽量不需要滚动;
  3. 再次,该函数会遍历其每一个子节点,依次计算它们的布局;每一个元素会实现自己的 layout 函数,根据特定的算法计算该类型元素的布局;
  4. 最后,节点根据它的子女们的大小计算得出自己的高度,整个过程结束。

读《webkit技术内幕》了解浏览器内核系列

  1. 读《webkit技术内幕》了解浏览器内核一
  2. 读《webkit技术内幕》了解浏览器内核二——内部架构
  3. 读《webkit技术内幕》了解浏览器内核三——资源与网络栈
  4. 读《webkit技术内幕》了解浏览器内核四——HTML、DOM以及CSS相关
  5. 读《webkit技术内幕》了解浏览器内核五——渲染基础
  6. 读《webkit技术内幕》了解浏览器内核六——硬件加速机制
  7. 读《webkit技术内幕》了解浏览器内核七——js引擎

写了个星期时间范围选择组件发到npm上

起因

还是在上家公司时,产品抄了头条的广告后台系统,几乎是把头条的UI大差不差的抄过来了,毕竟头条做的确实比淘宝、腾讯的好看些。

就因为产品抄了头条,我才第一次见到这种组件。不过当时写的非常low逼,bug应该多的一笔,最近自己开始重构这个组件,并且加上了半小时配置,今天终于把vue和react版本的都发上去,并且能用了,当然,肯定会有很多bug。

见:
vue-week-time-range-picker
react-week-time-range-picker

一开始看到这个组件时,我觉得确实好看,但是,我内心是拒绝的,因为我不知道怎么写。。。

头条也不会把代码给我看啊。。。

好在当时还没开始开发,产品们还在画着原型,老大就让我们先做这些基础组件,时间充足加上确实有点兴奋,感觉我能装逼。

过程

一开始开发一共就花了4天把,其实布局啥的,第一版完全抄的头条的,我甚至class名都没怎么改。

我先做的是点击事件,这个容易,但是后来做框选时发现问题了,框选用的 mousedown、mouseup、还有mousemove配合,我发现 mousedown 按下时会触发 click,不过仔细一想也是,都需要按下嘛。

既然冲突,那么 click 事件必须去掉,这个好理解。

其实这个组件最烦的就是框选还有下面的时间展示了。由于业务需求只要小时,不需要带半小时的,所以当时相对简单吧。

由于我是算法渣,真的,不懂,当时主要时间就花在数据处理上了,好在当时过关能用了,不过还是不够。

最近,我想把组件完善下,加上半小时配置,然后突然发现,之前的组件一堆bug,表格每个td的宽高都跟以前不同了,这肯定不行的,我这个非常依赖td的固定宽高的,所以也是自己慢慢去改css去调整,然后才确定每个td 16*20 的大小。

vue版本开发还算顺利,发包也顺利,测试也顺利,本以为10.1号就能把react版本写完并发布的,后来发现踩坑了,一直拖到今天。。。

有起码1天半的时间浪费在react版本发包上,就是因为我用 hooks,我这个组件依赖react,而hooks 要求必须用同样的react对象引用吧,所以我即使本地跑的再溜,npm拉下来测试直接死。。。

不过,刚刚终于在react github仓库issue里面找到了解决办法,真坑爹啊。

我几乎一天半都认为是我代码写的有问题或者我环境有问题,一直在看各种官方文档,殊不知,哎,确实是环境有问题,但这个问题也太变态了吧!!!

详见:react hooks写的组件发到npm上一直不能用,坑的一笔

重学js —— js数据类型:Number(一)

js数据类型:Number

注意

数字类型

ECMAScript 有两种内置数字类型:Number 和 BigInt。规范中,对于每个数字类型 T,使用 T::unit 形式表示进行相应数字类型的运算值。规范类型也有一些 抽象运算,例如 T::op 表示对指定名称 op 的给定操作。所有参数类型为 T。下表 结果 一列表示返回类型,以及该操作的某些调用是否有可能返回 abrupt completion

T表示数字类型,unit 表示该类型下的对应运算类型,例如 移位运算

数字类型运算表

调用形式 示例 被 ... 的求值语义调用 结果
T::unaryMinus(x) -x 一元 - 运算 T
T::bitwiseNOT(x) ~x 按位非运算 T
T::exponentiate(x, y) x ** y 求幂运算Math.pow(base, exponent) T,也可能抛 RangeError
T::multiply(x, y) x * y 乘法运算 T
T::divide(x, y) x / y 除法运算 T,也可能抛 RangeError
T::remainder(x, y) x % y 取余运算 T,也可能抛 RangeError
T::add(x, y) x++,++x,x + y 后缀++运算++前缀运算 以及 加法运算 T
T::subtract(x, y) x--,--x,x - y 后缀--运算--前缀运算 以及 减法运算 T
T::leftShift(x, y) x << y 左移 T
T::signedRightShift(x, y) x >> y 有符号右移 T
T::unsignedRightShift(x, y) x >>> y 无符号右移 T,也可能抛 RangeError
T::lessThan(x, y) x < y,x > y,x <= y,x >= y 关系运算 Boolean 或 undefined
T::equal(x, y) x == y,x != y,x === y,x !== y 相等运算 Boolean
T::sameValue(x, y) 对象内置方法,通过 SameValue( x, y ),测试精确值相等 Boolean
T::sameValueZero(x, y) Array,Map,以及Set 方法,通过 SameValueZero( x, y ) Boolean
T::bitwiseAND(x, y) x & y 按位与 T
T::bitwiseXOR(x, y) x ^ y 按位异或 T
T::bitwiseOR(x, y) x | y 按位或 T
T::toString(x) String(x) 许多表达式和内置函数,通过 ToString ( argument ) String

Number 类型

Number类型有 18437736874454810627R264 - 253 + 3)个值,表示为二进制浮点运算制定的符合IEEE标准指定的 IEEE 754-2019 64位双精度值,除了 9007199254740990R2R53R - 2R)表示不同,该值在ECMAScript中表示单一的特殊值 NaN。对ECMAScript代码来说,所有 NaN 值都难以互相区分。

将Number值存储到 ArrayBufferSharedArrayBuffer 中可能会观察到位模式不一定与ECMAScript实现使用的Number值的内部表示相同。

有两个特殊值:正无穷负无穷。出于说明目的,这些值也用符号 +∞-∞ 表示。(实际js代码中通过 +Infinity(简写成 Infinity) 和 -Infinity 表示)

其它的 18437736874454810624R2R64R - 2R53R)个值称为有限数。它们中一半是正数,一半为负数。每个有限正数和其对应的负数值大小相同。(绝对值一样)

注意存在 +0-0

18437736874454810622R2R64R - 2R53R - 2R)个有限非0数属于以下两种:

  • 它们中 18428729675200069632R2R64R - 2R54R)个数是标准的,形如 s × m × 2e
    • 其中 s 是 +1R 或 -1Rm 是小于 2R53R 且大于等于 2R52R 正的 数学整数e 是范围在 -1074R ~ 971R数学整数
  • 剩下的 9007199254740990R(2R53R - 2R)个值是非标准的,形如 s × m × 2e
    • 其中 s 是 +1R 或 -1Rm 是小于 2R52R 正的 数学整数e-1074R

请注意,所有不大于253的正负数学整数都可以在Number类型中表示。(数学整数0有 +0-0 两种表示)

如果有限数非零且用于表示它的 数学整数 m(以上述两种形式之一)是奇数,则它具有奇数有效位。否则,它有偶数有效位。

“the Number value for x”

规范中,短语 “the Number value for x”,其中 x 表示精确的实际数学量(有可能是 π 这种非理数),表示按以下方式选择的 Number 值。考虑Number类型所有有限值的集合,删除了 -0 并添加了两个在Number类型中无法表示的值,即 2R1024R+1R x 2R53R x 2R971R)和 -2R1024R-1R x 2R53R x 2R971R)。选择该集合中值与 x 最接近的成员。如果集合的两个值相等接近,则选择一个具有偶数有效位的值;因此,两个额外的值 2R1024R-2R1024R 被认为是偶数。

最终,用 +∞代表2R1024R;用 -∞代表-2R1024R

如果选择了 +0,仅当 x 小于零时才用 -0 替换;其他任何选择的值均保持不变。这些结果就是 x 的Number值。(此过程完全符合IEEE 754-2019 roundTiesToEven模式的行为。)

某些ECMAScript运算符仅处理特定范围内的整数,例如 -231 ~ 231 - 1,或 0 ~ 216 - 1。这些运算符能接受 Number 类型的任何值,不过会将这些值转换成运算符期望的范围内的 整数值。可以看 重学js —— 类型转换 中关于数值转换描述。

Number::unit 形式运算算法

Number::unaryMinus( x ) 减法

  1. 如果 xNaN,返回 NaN
  2. 返回 x 的负数结果;计算一个具有相同大小但符号相反的数字

Number::bitwiseNOT( x ) 按位非

~1 // -2
  1. 定义 oldValue! ToInt32(x)
  2. 返回对 oldValue 应用按位补码的结果。该结果是一个有符号32位 整数

Number::exponentiate( base, exponent ) 求幂

1 ** NaN // NaN
NaN ** +0 // 1
NaN ** -0 // 1
NaN ** 1 // NaN
2 ** +Infinity // Infinity
2 ** -Infinity // 0
1 ** +Infinity // NaN
1 ** -Infinity // NaN
0.1 ** +Infinity // 0
0.1 ** -Infinity // Infinity
(+Infinity) ** 1 // +Infinity
(+Infinity) ** -1 // 0
(-Infinity) ** 1 // -Infinity
(-Infinity) ** 2 // +Infinity
(-Infinity) ** -1 // -0
(-Infinity) ** -2 // +0
(+0) ** 1 // +0
(+0) ** -1 // +Infinity
(-0) ** 1 // -0
(-0) ** 2 // +0
(-0) ** -1 // -Infinity
(-0) ** -2 // +Infinity
(-1) ** 0.1 // NaN
  1. 如果 exponentNaN,结果是 NaN
  2. 如果 exponent+0,结果是 1,即使 baseNaN
  3. 如果 exponent-0,结果是 1,即使 baseNaN
  4. 如果 baseNaNexponent 非0,结果是 NaN
  5. 如果 abs(base) > 1 且 exponent+∞,结果是 +∞
  6. 如果 abs(base) > 1 且 exponent-∞,结果是 +0
  7. 如果 abs(base) 为 1 且 exponent+∞,结果是 NaN
  8. 如果 abs(base) 为 1 且 exponent-∞,结果是 NaN
  9. 如果 abs(base) < 1 且 exponent+∞,结果是 +0
  10. 如果 abs(base) < 1 且 exponent-∞,结果是 +∞
  11. 如果 base+∞exponent > 0,结果是 +∞
  12. 如果 base+∞exponent < 0,结果是 +0
  13. 如果 base-∞exponent > 0 且 exponent 是奇数,结果是 -∞
  14. 如果 base-∞exponent > 0 且 exponent 不是奇数,结果是 +∞
  15. 如果 base-∞exponent < 0 且 exponent 是奇数,结果是 -0
  16. 如果 base-∞exponent < 0 且 exponent 不是奇数,结果是 +0
  17. 如果 base+0exponent > 0,结果是 +0
  18. 如果 base+0exponent < 0,结果是 +∞
  19. 如果 base-0exponent > 0 且 exponent 是奇数,结果是 -0
  20. 如果 base-0exponent > 0 且 exponent 不是奇数,结果是 +0
  21. 如果 base-0exponent < 0 且 exponent 是奇数,结果是 -∞
  22. 如果 base-0exponent < 0 且 exponent 不是奇数,结果是 +∞
  23. 如果 base < 0 且 base 是有限的且 exponent 是有限的且 exponent 不是整数,结果为 NaN

base ** exponent 中如果 base1-1exponent+Infinity-Infinity,其结果与 IEEE 754-2019 不同。ECMAScript的第一版指定此操作结果为 NaN ,而 IEEE 754-2019 的更高版本指定结果为 1。出于兼容性原因,保留了历史ECMAScript行为。

Number::multiply( x, y ) 乘法

0.1 * NaN // NaN
0.1 * 0.2 // 0.020000000000000004

浮点数乘法的结果遵循 IEEE 754-2019 二进制双精度算法的规则:

  1. 如果操作数有一个是 NaN,结果为 NaN
  2. 如果两个操作数符号相同,那么其结果为正数,否则符号不同,结果为负数
  3. 无穷大乘以零会得到 NaN
  4. 无穷乘以无穷,结果也为无穷。符号由第2步规则决定
  5. 无穷乘以有限非0数,结果为有符号的无穷。符号由第2步规则决定
  6. 其他情况,既没有无穷也没有 NaN,使用 IEEE 754-2019 roundTiesToEven 模式计算乘积并将其舍入到最接近的可表示值。如果太大而无法表示,结果为正确符合的无穷。如果太小而无法表示,结果用正确符号的0表示。ECMAScript语言需要支持 IEEE 754-2019 定义的渐进式下溢。

Number::divide( x, y ) 除法

1 / NaN // NaN
Infinity / Infinity // NaN
Infinity / -Infinity // NaN
Infinity / 0 // Infinity
Infinity / 1 // Infinity
Infinity / -1 // -Infinity
1 / Infinity // 0
0 / 0 // NaN
0 / -Infinity // -0
3 / 0 // Infinity

ECMAScript不会执行 整数 除法。所有除法运算的操作数和结果均为双精度浮点数。

  1. 如果操作数有一个是 NaN,结果为 NaN
  2. 如果两个操作数符号相同,那么其结果为正数,否则符号不同,结果为负数
  3. 无穷除以无穷结果为 NaN
  4. 无穷除以0结果为无穷,符号由第2步规则决定
  5. 无穷除以非0有限数,结果为有符号的无穷,符号由第2步规则决定
  6. 有限数除以无穷,结果为0,符号由第2步规则决定
  7. 0除以0结果为 NaN;0除以任何其它数结果都是0,此时符号由第2步规则决定
  8. 非0有限数除以0结果为有符号的无穷,符号由第2步规则决定
  9. 其他情况,既没有无穷也没有 0 或 NaN,使用 IEEE 754-2019 roundTiesToEven 模式计算商并将其四舍五入为最接近的可表示值。如果太大而无法表示,则运算溢出;结果用正确符号的无穷表示。如果太小而无法表示,则运算下溢,结果用正确符号的0表示。ECMAScript语言需要支持 IEEE 754-2019 定义的渐进式下溢。

Number::remainder( n, d ) 取余

1 % NaN // NaN
Infinity % 1 // NaN
Infinity % 0 // NaN
1 % Infinity // 1
1 % -Infinity // 1
0 % 1 // 0
0.1 % 0.1 // 0

在C和C++中,余数运算仅接受整数操作数。在ECMAScript中,它也接受浮点操作数。

  1. 如果操作数有一个是 NaN,结果为 NaN
  2. 结果的符号等于被除数 n 的符号
  3. 如果被除数是无穷,或除数是0,结果为 NaN
  4. 如果被除数是有限的,而除数为无穷,结果为被除数
  5. 如果被除数是0,除数非0且有限,结果为被除数
  6. 其他情况,既没有无穷也没有 0 或 NaN,被除数 n 和除数 d 的浮点余数 r 由数学关系 r = n - (d x q)q 是整数,当 n / d 为负时才为负,当 n / d 为正时才为正,在不超过 nd 的数学商大小的情况下,其大小应尽可能大。使用 IEEE 754-2019 roundTiesToEven 模式计算 r 并四舍五入为最接近的可表示值。

篇幅过大,剩余的另起一章

读《webkit技术内幕》了解浏览器内核五——渲染基础

前言

前面介绍了HTML和CSS解析以及它们如何构建RenderObject对象树,最后又提到了布局,那么是不是这些任务完成后页面就出来了呢?

不是的。接下来还会构建 RenderLayer 树,然后才会进行 绘图 最终渲染并展示到网页上。

从这个系列开篇就介绍过 RenderObject 以及 RenderLayer 树,但是从来没有讲过它们到底是什么?内部长什么样子?

还有,我知道了大致渲染流程,但这仅仅是 流程,至于到底是通过什么 方式 进行渲染的,我对此还一概不知。

本篇将跟着书中介绍来了解下以上这些涉及到渲染基础的内容。

问题

什么是RenderObject树?

在DOM树中,某些节点是用户不可见的,也就是说这些只是起一些其他方面而不是显示内容的作用。

例如表示HTML文件头的 meta 节点,在最终的显示结果中,用户是看不到它的,书中称它为 非可视化节点。而另外的例如 bodydiv等节点是用来展示网页内容的,称为 可视化节点

对于可视化节点,WebKit需要将它们内容绘制到最终的网页结果中,所以WebKit会为它们建立相应的 RenderObject 对象。

一个 RenderObject 对象保存了为绘制DOM节点所需要的各种信息,例如样式布局信息,经过WebKit处理后,RenderObject 对象知道如何绘制自己。

这些 RenderObject 对象同DOM节点对象类似,它们也构成一棵树,称为 RenderObject树。RenderObject树基于DOM树构建,是为了 布局计算和渲染 等机制而构建的一种新的内部表示。

RenderObject树和DOM树中的节点是一一对应的吗?

不是。
基于以下三条规则才会为DOM树节点创建一个RenderObject对象:

  1. DOM树的document节点
  2. DOM树的可视化节点
  3. 某些情况下,WebKit需要建立匿名的RenderObject节点,该节点不对应于DOM树中的任何节点,而是WebKit处理上的需要,典型的例子就是匿名的 RenderBlock 节点

图例:

之前介绍的Shadow DOM,WebKit如何处理Shadow DOM树中的节点呢?

WebKit处理 Shadow DOM与普通DOM节点一样,需要创建并渲染RenderObject。

之前也有了解到,RenderObject树会和css样式结合然后构建RenderLayer树,这个RenderLayer树是什么?

首先需要知道一点,网页是可以 分层 的,有两点原因:

  1. 方便网页开发者开发网页并设置网页层次(如z-index
  2. 方便WebKit处理,简化渲染逻辑

因此,WebKit会为网页的层次创建相应的 RenderLayer 树。

一般来说,某个 RenderObject 节点的后代都属于该节点,除非WebKit根据规则为某个后代 RenderObject 节点创建了一个新的 RenderLayer 对象。

RenderLayer树基于 RenderObject 构建。RenderLayer 节点和 RenderObject 节点不是一一对应,而是一对多的关系。

除了 RenderLayer 树的根节点,一个 RenderLayer 节点的父节点就是 RenderLayer 节点对应的 RenderObject 节点的祖先链中最近的祖先,并且该 RenderObject 节点所在的 RenderLayer 节点不同于该 RenderLayer节点(比较绕人)。

基于上文,那些 RenderLayer 节点也构成了一棵 RenderLayer树。

每个 RenderLayer 节点包含的 RenderObject 其实是一棵 RenderObject 子树。

如何构建RenderLayer树呢?

其实要先了解哪些情况下的RenderObject节点需要建立新的RenderLayer节点,有以下一些基本原则:

  • DOM树的Document节点对应的RenderView节点
  • DOM树中的Document的子女节点,也就是HTML节点对应的RenderBlock节点
  • 显式的指定CSS位置的RenderObject节点
  • 有透明效果的RenderObject节点
  • 节点有溢出(overflow)、alpha或者反射等效果的RenderObject节点
  • 使用Canvas 2D和3D(WebGL)技术的RenderObject节点
  • video节点对应的RenderObject节点

构建过程比较简单,根据规则条件来判断是否需要建立 RenderLayer 对象,并设置父子兄弟关系即可,如图:

经过上面的了解,RenderObject对象已经知道如何绘制自己,但是,RenderObject对象用什么来绘制内容呢?

在WebKit中,绘图操作被定义了一个抽象层,就是 绘图上下文,所有绘图操作都是在该上下文中来进行的。

绘图上下文可以分成两种类型:

  • 用来绘制2D图形的上下文,称为2D绘图上下文
  • 绘制3D图形的上下文,称为3D绘图上下文

对于2D绘图上下文,既可以使用CPU来完成2D相关操作,也可以使用3D图形接口(如OpenGl)来完成2D相关操作。

具体的渲染方式是什么呢?

主要有两种方式:

  1. 软件渲染
  2. 硬件加速渲染

其实还有一种混合模式渲染。

什么是软件渲染?什么是硬件加速渲染?

如果绘图使用CPU来完成,那么称之为 软件绘图
如果绘图由GPU来完成,称为GPU硬件加速绘图

两种渲染方式还有什么其他的区别吗?

软件渲染不需要 合成,硬件加速渲染 需要合成

什么是合成呢?

首先,每个 RenderLayer 对象可以被想象成图像中的一个层,每个层一同构成了一个图像。理想情况下,每个层都有个绘制的存储区域,这个存储区域用来保存绘图的结果。最后,需要将这些层的内容合并到同一个图像中,书中称为 合成,使用了合成技术的称为 合成化渲染

为什么软件渲染不需要合成?

没必要。
软件渲染中,通常渲染的结果就是一个 位图,绘制每一层的时候都使用该位图,区别在于绘制的位置可能不一样,当然每一层都按照从后到前的顺序。也可以为每层分配一个位图,但关键是,一个位图已经解决所有问题了。

软件渲染使用的位图实际上就是一块CPU使用的内存空间。

为什么会有软件渲染、硬件渲染、混合(软件绘图的合成化)渲染三种方式呢?

因为三种方式各有各的优缺点和适用场景。

先了解一些渲染方面的基本知识:

对于常见的2D绘图,使用GPU来绘图不一定比使用CPU绘图在性能上有优势,例如绘制文字、点、线等,原因是CPU的使用缓存机制有效减少了重复绘制的开销而且不需要GPU并行性。
其次,GPU内存资源相对CPU内存资源来说较为紧张,而且网页分层使得GPU内存使用相对较多。

分析下三种方式的特点:

  • 软件渲染是浏览器最早使用的渲染方式,节省内存,但是只能处理2D绘图。
  • GPU硬件加速合成化渲染,对需要3D绘图的操作特别合适。当然,消耗更多的内存资源。但是另一方面,支持所有的HTML5定义的2D或者3D绘图标准。关于更新区域,如果需要更新某一层的一个区域,硬件加速渲染只需要重新绘制更新发生的层次;而软件渲染没有为每层提供后端存储,因而它需要将和这个区域有重叠部分所有层次的相关区域依次从后向前重新绘制一遍,代价更大。
  • 软件绘图的合成化结合前两种方式优点。

读《webkit技术内幕》了解浏览器内核系列

  1. 读《webkit技术内幕》了解浏览器内核一
  2. 读《webkit技术内幕》了解浏览器内核二——内部架构
  3. 读《webkit技术内幕》了解浏览器内核三——资源与网络栈
  4. 读《webkit技术内幕》了解浏览器内核四——HTML、DOM以及CSS相关
  5. 读《webkit技术内幕》了解浏览器内核五——渲染基础
  6. 读《webkit技术内幕》了解浏览器内核六——硬件加速机制
  7. 读《webkit技术内幕》了解浏览器内核七——js引擎

读《数据结构与算法 JavaScript描述》——实现一个栈结构

什么是栈?

  栈是一种高效的数据结构,数据只能从栈顶添加或删除!栈本质上是一种特殊的列表,栈内的元素只能通过列表的一端访问,这一端称为栈顶。
  也就有了“后进先出/先进后出”这种流行的说法。

如何实现栈?

实现栈之前,先要明确,栈支持哪些操作方法?用js实现栈的底层数据结构是什么?
为了方便理解与更容易的结合js已经提供的原生方法实现,这里采用数组来做实现栈的底层结构。

  1. 由于栈只能对顶部进行添加和删除,对应数组的最后一个元素。所以可以直接采用数组的pushpop方法。
  2. 栈可以清空,清空数组也很方便,所以需要实现一个clear方法。
  3. 栈需要一个方法来告知用户当前栈的长度,对标数组的length属性。

根据以上三点,就可以实现第一版的栈了:

function fakeStack () {
  this.data = [];
}

fakeStack.prototype.push = function () {
  this.data.push(...arguments)
}

fakeStack.prototype.pop = function () {
  this.data.pop()
}

fakeStack.prototype.length = function () {
  return this.data.length
}

fakeStack.prototype.clear= function () {
  this.data.length = 0;
}

// 测试
var s = new fakeStack();
s.push(1);
s.length(); // 1
s.pop();
s.length(); // 0
s.push(1);
s.clear();
s.length(); // 0

查阅书籍,发现栈还应该提供一个查看当前栈顶元素的方法,这也很好办,直接返回数组最后一个元素即可:

fakeStack.prototype.peek= function () {
  let len = this.data.length;
  // let len = this.length();
  return this.data[len - 1];
}

闭包:我让你云里雾里摸不着头脑,你怕不怕?

前言

原先在老博客里面有写过闭包相关知识,本以为懂了,但是前两天思考惰性求值时又有点迷糊了,这东西看来我还没到融会贯通的地步,只靠理论是不行的,还是要写例子,并且多写,闭包是典型的孰能生巧的技术。
原先博客里的闭包保留,这里主要翻译介绍stackoverflow上高达6055票的高赞回答:How do JavaScript closures work?,回答的还真不错,翻译过来既让自己再度学习,也给看到的人输送“国际知识”。

翻译正文

闭包并不是魔法

这篇文章讲解闭包是为了让程序员能够理解并通过js使用它。并不是为专家或功能程序员(不知道翻译对不对)而写的。

一旦你领悟到了闭包的核心概念,那么对于你来说闭包并不难以理解。但是,我们不能仅通过阅读学术文章或者一些学术方面的知识去理解闭包

这篇文章是针对具有主流语言编程经验的程序员,能够阅读并理解接下来的js函数:

function sayHello(name) {
  var text = 'Hello ' + name;
  var say = function() { console.log(text); }
  say();
}
sayHello('Joe');

一个关于闭包的例子

两句话总结:

  • 闭包是一种支持一等函数(函数在js里是“一等公民”)的方式;它是一个表达式,可以引用其作用域内的变量(当它被首次声明时),被赋值给变量,作为参数传递给函数,或作为函数结果返回。
  • 或者,闭包是当函数开始执行时分配的堆栈帧(翻译起来有点别扭),在函数返回后不会释放(就好像一个'堆栈帧'被分配在堆上而不是堆栈上!)

下面的代码返回了对函数的引用:

function sayHello2(name) {
  var text = 'Hello ' + name; // Local variable
  var say = function() { console.log(text); }
  return say;
}
var say2 = sayHello2('Bob');
say2();  // logs "Hello Bob"

大多数JavaScript程序员能理解如何将一个函数的引用返回给上述代码中的变量(say2)。如果你不理解,在学习闭包之前你应该先看看这方面的知识。使用C语言的程序员可能会认为这里的函数是作为指向另一个函数的指针然后被返回,并且会认为变量saysay2都是指向函数的指针。

C语言里面指向函数的指针和js里面函数的引用有非常关键的区别。在js里面,你可以将一个函数引用变量想象成既有指向函数的指针又指向闭包的隐藏指针。

上述代码中存在闭包,因为匿名函数function() { console.log(text);}声明在另一个函数(上例中的sayHello2())的内部。在js世界里,如果你在一个函数内部使用了function关键字,那么你正在创建一个闭包

C或者其他大多数与C相似的语言中,函数返回后,所有函数内的局部变量不能外部被获取,因为堆栈帧被销毁了。

在js中,如果你在一个函数内部声明另一个函数,当你调用返回的函数时,局部变量保持可访问状态。上面已经说明了,因为我们在函数sayHello2()返回之后才调用函数say2()。注意在上述代码中,我们调用了函数sayHello2()中局部变量text的引用。

say2.toString(); // "function() { console.log(text); }"

观察say2.toString()的输出,我们可以看到代码引用了变量text。匿名函数为什么可以引用保存了值为**'Hello Bob'变量text呢?这是因为函数sayHello2()的局部变量被保存在了闭包**内!

神奇的是,在JavaScript中,一个函数引用也对它创建的闭包有一个秘密引用——类似于委托是方法指针加上对对象的秘密引用。

更多的例子

因为某些原因,闭包似乎真的很难理解当你去阅读相关理论的话。但是当你看到一些例子的话,闭包是如何起作用的将会变得清晰易懂(我花了一些时间去理解它)。我建议仔细研究这些例子,直到你明白它们是如何工作的。如果你在没有完全理解它们如何工作的情况下开始使用闭包,你很快就会创建一些非常奇怪的bugs

例3

这个例子表明局部变量不是被复制的——而是通过引用来保存的。这就好像当外部函数退出时在内存中保留一个栈帧!

function say667() {
  // Local variable that ends up within closure
  var num = 42;
  var say = function() { console.log(num); }
  num++;
  return say;
}
var sayNumber = say667();
sayNumber(); // logs 43

例4

所有这三个全局函数都有一个对同一个闭包的共同引用,因为它们都是在一次调用setupSomeGlobals()中被赋值

var gLogNumber, gIncreaseNumber, gSetNumber;
function setupSomeGlobals() {
  // Local variable that ends up within closure
  var num = 42;
  // Store some references to functions as global variables
  gLogNumber = function() { console.log(num); }
  gIncreaseNumber = function() { num++; }
  gSetNumber = function(x) { num = x; }
}
setupSomeGlobals();
gIncreaseNumber();
gLogNumber(); // 43
gSetNumber(5);
gLogNumber(); // 5
var oldLog = gLogNumber;
setupSomeGlobals();
gLogNumber(); // 42
oldLog() // 5

这三个函数共享了同一个闭包——它们都被赋值引用函数setupSomeGlobals()的局部函数。(译者注:一开始定义了三个全局变量,但是没有赋值,当调用setupSomeGlobals()后,三个全局变量在该函数内被赋值,通过引用保存了三个不同的局部函数,他们三个与外部setupSomeGlobals()一起构成了一个闭包,所以他们三共享同一个闭包内的所有变量)。

注意上面的例子,如果你又一次调用setupSomeGlobals(),那么会创建一个新的闭包。原先的gLogNumbergIncreaseNumbergSetNumber三个变量所保存的引用将会被新闭包内的新函数覆盖。(在js中,无论何时你在一个函数中声明另一个函数,每次外部函数被调用时,其内部的函数也会被重新创建。)

例5

这个例子表明闭包包含所有在外部函数退出前——声明在其内部的局部变量。注意观察,局部变量alice 事实上是在匿名函数之后声明的。匿名函数先被声明,但是当这个匿名函数被调用时它却可以获得局部变量alice ,这是因为alice 与匿名函数在相同的作用域内(JavaScript做了变量提升)。另外sayAlice()()只是直接调用从sayAlice()返回的函数引用——它与之前的做法完全相同,但没有临时变量。

function sayAlice() {
    var say = function() { console.log(alice); }
    // Local variable that ends up within closure
    var alice = 'Hello Alice';
    return say;
}
sayAlice()();// logs "Hello Alice"

提示:注意变量say也在闭包内,可以被任何在sayAlice()内部声明的其它函数访问,或者它也可以在内部函数中递归访问。

例6(我栽在了这里)

这对许多人来说是一个真正的难题,所以你需要了解它。如果要在循环中定义函数,请务必小心:闭包中的局部变量可能并不像你想象中的那样工作。

您需要了解Javascript中的“变量提升”功能才能理解此示例。

function buildList(list) {
    var result = [];
    for (var i = 0; i < list.length; i++) {
        var item = 'item' + i;
        result.push( function() {console.log(item + ' ' + list[i])} );
    }
    return result;
}
function testList() {
    var fnlist = buildList([1,2,3]);
    // Using j only to help prevent confusion -- could use i.
    for (var j = 0; j < fnlist.length; j++) {
        fnlist[j]();
    }
}
 testList() //logs "item2 undefined" 3 times

译者注:我以为输出 "item2 3" 3 times,但其实是"item2 undefined" 3 times,可见我掌握的还是有问题,被绕进去了,希望看到的人能真正理解而不犯我这样的错!

result.push( function() {console.log(item + ' ' + list[i])}这段代码向resule这个数组中添加了3次对同一个匿名函数的引用。如果你对匿名函数不熟悉,不妨想象成下面这样:

pointer = function() {console.log(item + ' ' + list[i])};
result.push(pointer);

注意当你运行这个例子时,item2 undefined被打印三次!就跟先前的例子一样,对函数buildList 内的局部变量(result,i,item)而言只有一个闭包。在fnlist[j]()这一行时调用匿名函数;他们都使用相同的单个闭包,并且他们使用该闭包内的当前iitem值。(这里i值为3,因为循环已经完成并且item值为item2)。请注意,我们从0开始索引,因此item的值为item2。 而i ++会将i增加到3。(译者注:我就是在这里踩得坑,我以为i === 2,但是接下来i需要自增1来结束循环,最终闭包内保存的应该是结束循环时i的值;至于item为何不是item3,因为当i === 3时不满足循环条件,循环结束,所以itemitem2)。

当使用块级作用域声明变量item(通过let)代替用var声明的函数作用域变量看看会发生什么,可能会有帮助。如果发生了这种变化,那么数组result中的每个匿名函数都有自己的闭包(译者注:将var item = 'item' + i 改为let item = 'item' + i);那么这个例子输出将会像下面这样:

item0 undefined
item1 undefined
item2 undefined

如果变量i也用let声明,输出会变成下面这样:

item0 1
item1 2
item2 3

例7

最后一个例子,每次调用主函数(外部函数)都会创建一个独立的闭包。

function newClosure(someNum, someRef) {
    // Local variables that end up within closure
    var num = someNum;
    var anArray = [1,2,3];
    var ref = someRef;
    return function(x) {
        num += x;
        anArray.push(num);
        console.log('num: ' + num +
            '; anArray: ' + anArray.toString() +
            '; ref.someVar: ' + ref.someVar + ';');
      }
}
obj = {someVar: 4};
fn1 = newClosure(4, obj);
fn2 = newClosure(5, obj);
fn1(1); // num: 5; anArray: 1,2,3,5; ref.someVar: 4;
fn2(1); // num: 6; anArray: 1,2,3,6; ref.someVar: 4;
obj.someVar++;
fn1(2); // num: 7; anArray: 1,2,3,5,7; ref.someVar: 5;
fn2(2); // num: 8; anArray: 1,2,3,6,8; ref.someVar: 5;

概要

如果一切似乎完全不清楚,那么最好的办法就是通过例子去实践。阅读解释(纯理论知识)比理解示例要困难得多。我对闭包和栈帧的解释等在技术上并不正确——它们都是为了帮助理解而进行的粗略简化。一旦基本想法得到了解决,您可以稍后获取详细信息。

最后一点:

  • 无论何时在一个函数中使用函数,都会创建闭包
  • 无论何时在函数中使用eval(),都会使用闭包。你eval()的内容可以引用局部变量,你甚至可以在eval()内创建新的局部变量通过eval('var foo = …')这种方式。
  • 当你在函数内部使用new Function(...)(函数构造器),却不会创建一个闭包。(这个新的函数不能引用外部函数的局部变量)
  • JavaScript中的闭包就像保留所有局部变量的副本一样,就像退出函数时一样。
  • 可能最好认为闭包总是被创建成一个函数的入口,并且局部变量被添加到闭包中。
  • 每次调用具有闭包的函数时,都会保留一组新的局部变量(假定该函数在其中包含一个函数声明,并且对该函数的内部函数引用或者返回,或者以某种方式为其保留外部引用)
  • 两个函数可能看起来像是具有相同的源内容,但由于它们的“隐藏”闭包而具有完全不同的行为。我不认为JavaScript代码实际上可以找出函数引用是否有闭包。
  • 如果您正在尝试执行任何动态源代码修改(举个例子:myFunction = Function(myFunction.toString().replace(/Hello/,'Hola'));),如果myFunction 是一个闭包的话这段代码不会起作用(当然,你永远不会想到在运行时做源代码字符串替换,但是。。。)
  • 可以在函数内的函数声明中获得函数声明——你可以在多个级别上获得闭包
  • 我认为闭包通常是函数和被捕获变量的一个术语。请注意,我不使用这篇文章中的定义!
  • 我怀疑JavaScript中的闭包不同于正常函数式语言。

链接

感谢

如果你刚刚学习闭包(无论在哪!),我对任何有关您可能建议的更改可能会使本文变得更清楚的任何反馈感兴趣。发送邮件到morrisjohns.com(morris_closure @)。 请注意,我不是JavaScript的专家,也不是闭包

http!到!https!到!http2!到!开炮!!!

前言

毕竟是做前端开发的,主要跟web打交道,那么客户端与服务端怎么传输的得了解一下。当然,绕不开htpp家族了,最重要的是,现在工作对技术要求越来越高,如果去面大厂,估计都要问http相关知识,不可不学啊。
我自己就不写分析了,毕竟这块我是真的菜,好在网上有很多资源,我收藏那些遇到的且我认为讲的比较好的链接,如果有人有更好的收藏,能不能也在评论中贴出来,大家共同学习嘛~

要学的好多,我要抱抱!

相关链接

http标准

  1. woai30231提炼的关于《HTTP权威指南》每章的知识点总结
  2. Http状态码
  3. HTTPS 的故事 有趣
  4. 图解 HTTPS:Charles 捕获 HTTPS 的原理 该篇文章内部链接很多,很丰富
  5. http2讲解
  6. HTTP/2.0 相比1.0有哪些重大改进?
  7. HTTP/2 资料汇总 这篇文章也很好!看了他的http/http2专题,很详细了,早知道我就不开这个issue了。。。
  8. HTTP 缓存机制一二三
  9. 一文读懂前端缓存
  10. TCP协议专场
  11. TCP三次握手和四次挥手
  12. 强缓存和协商缓存

拓展

  1. HTTP请求中的Form Data与Request Payload的区别
  2. 使用URLSearchParams处理axios发送的数据
  3. 深入研究:HTTP2 的真正性能到底如何
  4. webpack & HTTP/2
  5. 闲话 CDN
  6. 大公司里怎样开发和部署前端代码?

this: 嘿嘿嘿,你怕不怕我?

怕!(认真脸)

一开始想自己写关于this总结的,奈何自己水平有限,写出来也不够全面系统,所以这里为了this单独开一个issue,用于收罗我所遇到的关于讲解this较好的文章,最好做到由浅入深,如果有看客能看到并有更好的资源,请在评论里贴出来,让我加进来,哈哈哈哈哈,大家互帮互助嘛~

  1. this 的值到底是什么?一次说清楚 by 方应杭
  2. 你怎么还没搞懂 this? by 方应杭
  3. 深入浅出 JavaScript 关键词 -- this 重点看
  4. JavaScript中的this陷阱的最全收集--没有之一

真的明白了吗?巩固下!

例1:

var obj1 = {
  fn: function() {
    console.log(this)
  }
}
obj1.fn(); // obj1
var o = obj.fn;
o(); // window 非严格模式下

能分别解释吗?
我尝试下:

  1. obj1.fn(); 打印输出obj1这个对象,因为此时fnobj1对象调用,所以this指向obj1
  2. o()在非严格模式下打印输出window,因为此时变量o引用obj1对象的fn属性所指向的函数,即此时oobj1.fn都只是指向存在内部的函数的引用,所以,此时直接o()相当于window.o()即window在调用堆中的函数,所以函数内打印的thiswindow。这里的var o = obj1.fn其实并不是保存obj1对象!!!而是告诉o,你可以跟我的fn属性引用同一个函数,但咱两不一样。

例2:

var obj2 = {
  fn: () => { console.log(this) }
}
obj2.fn(); // window 非严格模式下

例2为什么是window呢?
老实说,我以前也一直不知道为什么,只会死记硬背,知道我看了深入浅出 JavaScript 关键词 -- this一文,文中重点加粗的强调:

箭头函数按词法作用域来绑定它的上下文,所以 this 实际上会引用到原来的上下文

词法作用域?什么鬼???
一开始我也懵逼的,但是我依稀记得好像看过那么一丢丢《你不知道的js》,书上就有,我还写过一篇博客来记录,可惜当时菜,其实没怎么懂,接下去的内容又没去看,所以记忆不深,怪自己!

词法作用域在你在写代码时将变量写在哪里来决定的,而不是当执行时才确定! 一定要记牢这句话!!!

也就是说,当我们此时obj2.fn()时,我们以为它和例1一样,但由于内部fn函数时箭头函数,所以跟我们原先的理解完全不同了。该箭头函数其实在定义好obj2对象时就确定了它的词法作用域

它的词法作用域是什么?
这里,必须牢记,js里面没有块级作用域,只有全局作用域和函数作用域(witheval不提)。那么,当我定义obj2时,箭头函数的词法作用域是谁?
全局作用域!所以,这里打印输出thiswindow

有人会说es6的let声明能形成块级作用域,如果上例用let来声明会不会表现的不同呢?我来试试:

let obj2 = {
  fn: () => { console.log(this) }
}
obj2.fn(); // window 非严格模式下

即使换成let声明,结果依然不变!

例3:

如果把它放进函数里面呢?试试:

function test () {
  var obj2 = {
    fn: () => {console.log(this)}
  }
    return obj2.fn
}
test()(); // window 非严格模式下

这里obj2明明已经放进函数test里面了啊,它的词法作用域应该就是test函数作用域啊,那么为何打印输出依然是window呢?
其实,这里等价于function test() {console.log(this)}!为什么这么说?因为此时我们都知道箭头函数词法作用域是test的函数作用域,而该函数作用域的this是什么?当然是看它被谁调用了啊!这里是window调用的它!

例4(与例3对比):

明白了吗?不明白看个对比可能更好点:

var f = {
  fn : function () {
    var obj = () => {console.log(this)}
    return obj
  }
}
f.fn()(); // {fn: ƒ}

这里的箭头函数的词法作用域fn所指向的函数的作用域,那么只要看谁调用fn谁就是this

例5

箭头函数告一段落,继续回到正常的函数中,看看闭包中的this

var f = {
  fn : function () {
    var obj = function(){console.log(this)}
    return obj
  }
}
f.fn()();

这里为什么是undefined呢?
注意,这里可不是箭头函数了!其实这里等价于var fn = f.fn(); fn();这样一拆分,是不是很好理解了呢?

例6:

var arr = [1,2,3];
arr.map(function(){
  console.log(this)
})
// 打印三次window 非严格模式

why???
这里估计得先了解下map函数的实现才能很好理解,我先前贴出的链接里面有讲解,仔细看的肯定会知道原因。
其实map里面的匿名函数是作为参数传递进去的,然后在map内部直接执行没有其他变量去调用它,所以在非严格模式下自然就是window了。

例7

看一个正常函数的闭包变异案例:

function Thing() {};
Thing.prototype.foo = "bar";
Thing.prototype.logFoo = function(){
  var info = 'attempting to log this.foo:'
  function doIt() {
    console.log(info, this.foo)
  }
  console.log(this)
  doIt()
}
thing.logFoo();
// Thing {}
// attempting to log this.foo: undefined

注意,这里不是箭头函数,不用考虑词法作用域!只需要看谁调用它即可。此例中直接在函数内部执行了doIt(),没有谁调用它。所以doIt内部的this在非严格模式下指向window,而window内没有foo属性,所以是undefined

例8

var a = 20;
var obj = {
  a: 10,
  c: this.a + 20,
  fn: function () {
    return this.a;
  }
}

console.log(obj.c);
console.log(obj.fn());
// 40
// 10

我想,上例第二个打印10应该都能理解了,但是可能会在obj.c打印40这里容易懵逼。上例中cobj的属性,但是它并没有引用一个函数,当普通函数被调用时,this指向调用该普通函数的对象,但是如果调用的不是普通函数,那么情况就和箭头函数一样了,此时this应该由词法作用域确定!
牢记上面这段话,回到例子中来,obj.c指向的并不是普通函数,所以在它被写下来时就已经确定了它的词法作用域是全局作用域,所以thiswindowthis.a + 20等价于window.a + 20

例9

擦擦擦,又犯错误了!!!

function fn (){ console.log(this) }
var arr = [fn]
arr[0]()
// [f]

这里想错了,fn是普通函数,应该看它被谁调用!这里其实是arr在调用!!!

注意,不能对 this 直接赋值!例如:this = null,会报左边赋值无效!

总结

this是js的一个难点,有很多人凭工作经验去理解它,一般情况下他可能不会踩什么大坑,但一旦出现出现经验无法解决的坑,就很捉急了,所以建议在有一段开发经验后不要忘了去看一下深入基础的知识,好好巩固,把基础打牢,才能建高楼大厦!

读《webkit技术内幕》了解浏览器内核三——资源与网络栈

前言

前一篇,我了解了WebKit的内部架构、多线程以及多进程。继续深入的话还有线程内部消息和任务是如何处理的等等问题,这些会在未来去记录。

本篇继续按照《webkit技术内幕》目录来进行记录,这篇主要侧重于资源加载和网络栈

日常开发中,我们应该知道浏览器会缓存一些资源,尤其是静态资源,我们可不想每次打开/刷新都要重新去请求静态资源,这简直太浪费时间了!

为此,浏览器都引入了缓存机制

问题

资源缓存机制怎么实现的?

基本**:建立一个资源的缓存池,当WebKit需要请求资源时,先从资源池种查找是否存在相应的资源。

  • 如果有,取出使用;
  • 没有,创建一个新的CachedResource子类对象,并发送请求给服务器,WebKit收到资源后将其设置到该资源类的对象中,以便于缓存后下次使用。(这里的缓存指的是内存缓存

WebKit通过什么特定标识去资源池中查找对应的资源呢?

通过URL
标记资源唯一性的特征就是资源的URL。

这也意味着,假如两个资源有不同的URL,但是它们的内容完全一样,也会被认为是两个不同的资源。

当WebKit找到资源后,如何加载?

依靠资源加载器

WebKit有三种加载器:

  1. 针对每种资源类型的特定加载器
  2. 资源缓存机制的资源加载器,所有特定加载器都共享它来查找并插入缓存资源
  3. 通用的资源加载器——ResourceLoader类,也被所有特定加载器共享。从网络/文件系统获取资源时使用

能大概说说资源加载的过程吗?

还是看图说话吧,以加载某个img为例:

资源加载是同步的吗?

大部分资源加载都是异步的。

但是某些特别的资源加载会阻碍当前WebKit的渲染过程,例如js文件。

当js文件加载阻碍主线程,后面可能还有许多需要下载的资源,WebKit怎么做呢?

这种情况下,WebKit会启动另外一个线程去遍历后面的HTML网页,收集需要的资源URL,然后发送请求,这样就可以避免被阻碍。与此同时,WebKit能并发下载这些资源,甚至并发下载js代码。这种机制对于网页的加载提速非常明显。

有个问题,资源放进缓存池中会一直存在吗?

不会,资源有生命周期。

资源的生命周期是什么呢?

资源池采用 LRU(Least Recent Used 最近1最少使用)算法来进行新老资源的替换。

另一个问题,当资源放进资源池后,WebKit如何判断下次使用时是否需要更新该资源?

  • 对于某些资源,WebKit需要直接重新发送请求;
  • 对于很多资源,利用HTTP协议减少网络负载(HTTP缓存)

那么,资源如何被加载使用呢?是不是所有进程都能直接拿到它?

不是的,资源统一交由Browser进程来处理

处于安全性考虑,Renderer进程没有权限去直接获取资源,它只能通过 进程间通信 将任务交给Browser进程来完成。

进程间通信涉及到内部特定的一些类,可能每个基于WebKit的浏览器内部类名都不同。Chromium中是IPCResourceLoaderBridge

Chromium中主要是 ResourceLoader 类负责Browser进程中有关资源的总体管理任务。对于同步/异步两种请求方式,使用 SyncResourceHandleAsyncResourceHandle 类。

前面了解了资源加载,但是对于资源请求并没有过多的介绍,这里主要涉及到网络栈相关知识了。包括TCP三次握手、四次挥手,DNS域名解析,Cookie机制,磁盘本地缓存,安全机制,用户代理等。

什么是TCP三次握手、四次挥手?

可以看:https://lizhongzhen11.github.io/2018/03/16/TCP%E6%8F%A1%E6%89%8B%E4%B8%8E%E6%8C%A5%E6%89%8B/

什么是DNS域名解析?

可以看:https://www.zhihu.com/question/23042131/answer/66571369

Chromium如何保证安全又高速的网络栈呢?

使用了很多新技术,包括DNS预取和TCP预连接、HTTP管线化以及SPDY协议等。

什么是DNS预取和TCP预连接呢?

一次DNS查询平均时间大概是60~120ms之间或者更长,而TCP三次握手时间大概也是几十毫秒或者更长。

为了有效减少这段时间,Chromium通过 Predictor 机制来实现DNS预取和TCP预连接。

首先是DNS预取。利用现有的DNS机制,提前解析网页中可能的网络连接。具体来讲,当用户正在浏览当前网页的时候,Chromium提取网页中的超链接,将域名抽取出来,利用比较少的CPU和网络带宽来解析这些域名或IP地址,这样一来,用户根本感觉不到这一过程。(书中原文)

注意,DNS预取针对多个域名采取并行处理方式,每个域名的解析须由新开启的一个线程来处理,结束后此线程退出。

然后是TCP预连接。同DNS预取一样,追踪技术不仅应用于网页的超链接,当用户在地址栏中输入地址,如候选项同输入的地址很匹配,则在用户敲下回车键获取该网页前,Chromium就已经开始尝试建立TCP连接了。

如何应用

了解了以上有关资源/网络栈的知识后,我该如何去高效的使用资源呢(优化网页性能)?

可以从一下几点来考虑:

  • 减少链接的重定向。
  • 利用DNS预取机制。
  • 避免错误的链接请求,包含失效的。
  • 合并一些资源
  • HTML中内嵌小型资源。如图片使用base64编码。

读《webkit技术内幕》了解浏览器内核系列

  1. 读《webkit技术内幕》了解浏览器内核一
  2. 读《webkit技术内幕》了解浏览器内核二——内部架构
  3. 读《webkit技术内幕》了解浏览器内核三——资源与网络栈
  4. 读《webkit技术内幕》了解浏览器内核四——HTML、DOM以及CSS相关
  5. 读《webkit技术内幕》了解浏览器内核五——渲染基础
  6. 读《webkit技术内幕》了解浏览器内核六——硬件加速机制
  7. 读《webkit技术内幕》了解浏览器内核七——js引擎

前端知识点链接收藏

杂记

1 TC39委员会
2. Standard ECMA-262规范
3. ECMAScript 6 入门
4. V8 是什么?
5. HTML 5.2 W3C Recommendation, 14 December 2017 规范
6. Cascading Style Sheets Level 2 Revision 2 (CSS 2.2) Specification
7. 浏览器的工作原理
8. 当你在浏览器中输入 google.com 并且按下回车之后发生了什么?
9. Introducing Riptide: WebKit’s Retreating Wavefront Concurrent Garbage Collector
10. Tasks, microtasks, queues and schedules(宏任务与微任务)
11. 5分钟彻底理解Object.keys
12. CSS性能优化的8个技巧 PS:里面的知识点链接也很好
13. 网站性能优化实战——从12.67s到1.06s的故事
14. An Introduction to Source Maps
15. Performance Calendar
16. css-shorthand-longhand-notations
17. 现代 JavaScript 教程

stackoverflow收藏

  1. generator函数前面为什么设置了一个星号(*)?
  2. 在JavaScript中使用严格的操作以及它背后的原理是什么?
  3. event.preventdefault vs return false
  4. 如何去理解或解释js闭包?
  5. css url()中要不要加引号?

360奇舞团收藏

  1. 每个前端工程师都应该了解的图片知识(长文建议收藏)
  2. 关于移动端适配,你必须要知道的
  3. 如何优雅地取数值的整数和小数部分

前端早读课收藏

  1. RGB、HSL、Hex网页色彩,看完这篇全懂了

知乎收藏

  1. require,import区别?
  2. 浏览器输入 URL 后发生了什么?
  3. 反向代理为何叫反向代理?
  4. 网上都说操作真实 DOM 慢,但测试结果却比 React 更快,为什么?
  5. DNS原理及其解析过程

掘金收藏

  1. 谁说前端不需要懂-Nginx反向代理与负载均衡
  2. 为什么视频网站的视频链接地址是blob?
  3. 从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理 (强烈建议看完)
    作者自己的博客地址

segmentfault收藏

  1. 怎样阅读 ECMAScript 规范?

腾讯云服务器安装mysql踩坑记录--百度各种博客始终对不上

气死了!

  最近在家闲着,一边玩游戏一边帮老妈做个小程序项目,其实主要给自己练手。前后台终于完成,想到曾经上了一波腾讯云的车,便打开看了下还有一个多月到期,那就继续用吧,想着曾经有装过阿里云服务器相关环境的经验,腾讯云应该差不多吧,事实上,差很多。。。

过程

  1. 下载个xshell,弄个破解版的就好,同时下载个xftp,一样破解版的;
  2. 登录腾讯云控制台,找到自己的实例,可以看到IP地址,复制公网对应的IP地址(有个 公 字);
  3. 用xshell和xftp连接服务器,账号密码可以在腾讯云消息中心里的站内信找到;xshell连接:https://jingyan.baidu.com/article/ed2a5d1f6b31af09f7be1748.html
  4. 连接成功,开始安装mysql!
  5. 下载rpm文件,可在任意文件夹下下载,命令:wget https://repo.mysql.com//mysql57-community-release-el7-11.noarch.rpm
  6. 安装rpm:rpm -ivh mysql57-community-release-el7-11.noarch.rpm 。确认的时候输入y就好了,安装完成之后,在/etc/yum.repos.d目录下新增了两个文件,mysql-community.repo和mysql-community-source.repo
  7. yum 安装mysql,yum install mysql-community-server
  8. 启动mysql服务:systemctl start mysqld
  9. 设置mysql开机启动:systemctl enable mysqld, systemctl daemon-reload
  10. 我一开始继续按照网上教程,到了set password for 'root'@'localhost'=password('MyNewPass!')这一步时根本没反应,不管怎么试都没效果,真的醉了!!!后来,直接去查找my.cnf文件,命令:find / -name my.cnf,我的在/etc/my.cnf
  11. 修改/etc/my.cnf文件,命令:vi /etc/my.cnf,如果还处于mysql命令模式下,需要先输入exit退出,然后才能修改。
  12. 在[mysqld]下添加编码配置:
    [mysqld] 
    character_set_server=utf8 
    init_connect=’SET NAMES utf8’
  13. 重启mysql,命令:systemctl restart mysqld
  14. 查找root密码,mysql安装完成之后,在/var/log/mysqld.log文件中给root生成了一个默认密码。通过下面的方式找到root默认密码,然后登录mysql进行修改,命令:grep 'temporary password' /var/log/mysqld.log
  15. 如果上一步命令没反应,可能是你之前装过mysql又没有卸载干净,需要删除原来安装过的mysql残留的数据(这一步非常重要,问题就出在这),命令:rm -rf /var/lib/mysql;然后重启mysql(第13步的命令),再然后查找密码(第14步);
  16. 登录mysql,密码为刚才查找的密码,命令:mysql -u root -p
  17. 修改登录密码:set password for 'root'@'localhost'=password('你的密码'),如果下面出现Query OK等字段,说明成功;当然,也有可能失败,很大概率是密码安全策略问题,mysql要求密码大小写+特殊字符,你如果不愿意的话,百度下mysql密码安全策略修改
  18. 退出,命令:exit
  19. 重复第13步
  20. 测试刚刚设置的密码有没有生效,登录mysql,第16步的命令,密码换成你改的
  21. 如果成功,会进入mysql命令模式中,输入use mysql
  22. 输入update user set host = ‘%’ where user =’root’,允许非服务器本地连接该数据库
  23. 授权给root账号,GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY 'Lizz214856+' WITH GRANT OPTION;
  24. 最后记得在腾讯云服务器控制台里面,找到安全组,放开3306端口,也可以全都放开,自己选择。
  25. 可以用本地navicat连接了

吐槽

网上关于云服务器安装环境的教程一大堆,百度搜出来前几个虽然各有各的用处,但是总有各种坑或者各种不齐全,尼玛,写成这样也好意思发出来?最可笑的是有的文章命令行都有错别字,我真的是醉了!!!

重学js —— 类型转换

抽象运算 —— 类型转换

阅读这章之前,先思考下面代码:

+true // 1
+false // 0

+null // 0
+undefined // NaN

+'-0' // -0
+'Infinity' // Infinity
+'-Infinity' // -Infinity

''+(-0) // "0"
''+(-Infinity) // "-Infinity"

Boolean(NaN) // false
Boolean(Infinity) // true
Boolean(-Infinity) // true

Boolean(Symbol()) // true
+Symbol() // 报错
''+Symbol() // 报错
Symbol().toString() // "Symbol()"

// 对象转换
+{} // NaN
+[] // 0
+[1] // 1
+[Infinity] // Infinity
+[-0] // 0
+ [1, 2] // NaN
+[{}] // NaN
''+[] // ""
''+[1] // "1"
''+[1,2,3] // "1,2,3"
''+[Infinity, Infinity] // "Infinity,Infinity"
''+{} // "[object Object]"
''+function test (){} // "function test (){}"
[1, 2] + [2, 1] // '1,22,1'

老实说,我不能完全正确说出结果,也不能保证说出 为什么?

正因为我的js基础如此之垃圾,所以阅读规范中关于类型转换一章并翻译,同时配合MDN进行学习,希望有底气的回答上面代码的表现行为及原因。

正文

抽象运算不是ECMAScript语言的一部分;规范中定义它们仅是为了帮助指定ECMAScript语言的语义。

当需要时,ECMAScript会自动隐式执行类型转换。类型转换抽象运算只能接收 ECMAScript语言类型,不能接收 规范类型。

BigInt类型没有隐式类型转换;程序必须显式的调用BigInt进行类型转换。

ToPrimitive ( input [ , PreferredType ] )

将输入的input转换为非Object类型值。如果对象能够转换为多个原始类型,则可以使用可选提示PreferredType来确定需要转换成哪个类型。

  • 如果inputObject类型
    • 如果 PreferredType 不存在,定义过程变量 hint 赋值为 "default"
    • 如果 PreferredTypeString,定义过程变量 hint 赋值为 "string"
    • 否则,
      • 断定 PreferredTypeNumber
      • 定义过程变量 hint 赋值为 "number"
    • 定义过程变量exoticToPrim,执行GetMethod(input, @@toPrimitive)并赋值给exoticToPrim
    • 如果exoticToPrim不是undefined
      • 定义过程变量 result,调用 Call(exoticToPrim, input, « hint ») 并赋值给 result
      • 如果 result 不是 Object 类型则返回 result
      • 否则报类型错误
    • 如果 hint"default",将 hint 赋值为 "number"
    • 返回 调用OrdinaryToPrimitive(input, hint)的返回值
  • 如果input一开始就不是Object类型,直接原路返回

注意:如果没传 PreferredType,那么默认就是 number。 但是可以通过定义一个 @@toPrimitive 来覆盖默认行为。DateSymbol对象会覆盖默认的ToPrimitive行为。当没有PreferredType时,Date默认转为string

OrdinaryToPrimitive ( O, hint )

主要是为了将 Object 转为 原始数据类型。

  • O 必须是对象
  • hint 必须是 String 类型且值为 "string""number"
  • 如果 hint"string"
    • 定义过程变量 methodNames,值为List « "toString", "valueOf" »
  • 如果 hint"number"
    • 定义过程变量 methodNames,值为List « "valueOf", "toString" »
  • 顺序遍历 methodNames 这个List,定义过程变量 name 来缓存List中的每个值
    • 定义过程变量 method,执行 Get(O, name)赋值给 method
    • 执行 IsCallable(method),如果为 true
      • 定义过程变量 result,执行 Call(method, O)赋值给 result这里本质上就是在执行 valueOf() 或 toString(),先后顺序看methodNames这个List
      • 如果 result 不是 Object,则返回 result
  • 如果以上都不满足,报类型错误

ToBoolean ( argument )

直接看表就好:

参数类型 结果
Undefined false
Null false
Boolean argument
Number +0, -0, NaN 都返回 false,其余都是 true
String 空字符串即长度为0的返回false,否则都是true
Symbol true
BigInt 0n 返回 false,其余都是true
Object true

ToNumeric ( value )

顾名思义,转为数值的,只不过有NumberBigInt两种可能。

ToNumber ( argument )

这个是转换为Number类型的了。见下表:

参数类型 结果
Undefined NaN
Null 0
Boolean true => 1, false => +0
Number argument
String string转number
Symbol 类型报错
BigInt 规范目前写的有问题,见tc39/ecma262#1766
Object 先转为原始值;然后将原始值转为Number

注意:

2019.11.14号,ECMAScript262规范目前规定传入BigInt参数会报错,从测试来看,使用 + 操作应该默认的是ToNumber,符合要求;但是使用Number()却能正常转换,而该算法内部依然使用ToNumber,却不报错?

+0n // Uncaught TypeError: Cannot convert a BigInt value to a number
Number(+0n) // Uncaught TypeError: Cannot convert a BigInt value to a number
Number(0n) // 0

我去知乎提过这个问题,有大佬指出这是规范编写时的bug,还没有修复,见知乎

根据链接我去查看了issue里面的其它链接,ToNumber ( argument )这里依然是抛类型错误,但是下面的 注意 告诉我:

该规范的主要设计决策是禁止隐式转换,并迫使程序员自己使用显式转换。

此外,最新的BigInt 文档补丁里的Number(value)方法内部把ToNumber(value) 改成 ToNumeric(value)了

String转Number

针对String转Number,如果语法无法将String解释为StringNumericLiteral的扩展,则ToNumber的结果为 NaN

该语法的结尾符号全部由Unicode基本多语言平面(BMP)中的字符组成。因此,如果字符串包含任何成对或不成对的前导代理尾随代理代码单元,则ToNumber的结果将为NaN。

注意:

  • StringNumericLiteral可以包括 前导 和 尾随空白 以及 行终止符
  • 十进制的StringNumericLiteral可以具有任意数量的前导 0 数字
  • 十进制的StringNumericLiteral可以包含 +- 表示其符号
  • 空 或 仅包含空格 的StringNumericLiteral会转换为 +0
  • Infinity-InfinityStringNumericLiteral 认可,但它们不是 NumericLiteral
  • StringNumericLiteral 不能包含 BigIntLiteralSuffix

String类型的数字转换为Number类型的数字见:https://tc39.es/ecma262/#sec-runtime-semantics-mv-s

经过测试,小数点如果超过15位,那么会看第16位的值,然后不断进行四舍五入。本质还是和Number类型一样的。

测试:

+'9.999999999999999999999' // 10
+'9.1234567891234567891234' // 9.123456789123457
+'9.123456789999999' // 9.123456789999999
+'9.1234567899999991234567' // 9.123456789999999
+'9.1234567899999999234567' // 9.12345679
+'9.123456789123499' // 9.123456789123498
+'99999999999999999999' // 100000000000000000000

ToInteger( argument )

  • 先执行ToNumber(argument)并将值赋给过程变量number
  • 如果numberNaN,则返回 +0
  • 如果number+0, -0, +Infinity 或 -Infinity,则直接返回number(这里用Infinity代替规范中的符号)
  • 以上都不符合,则返回与number相同符号的 Number类型值,其大小是 floor(abs(number))。先取正,再四舍五入。

ToInt32 ( argument )

argument 转成32位(4字节)有符号的整数,取值范围在 Math.pow(2, -31) ~ Math.pow(2, 31) - 1 之间。

  • 先执行 ToNumber(argument) 并将值赋给过程变量 number
  • 如果 numberNaN, +0, -0, +Infinity-Infinity,则返回 +0
  • 定义过程变量 int,其值是 与 number 相同符号的 Number类型值且其大小是 floor(abs(number))
  • 定义过程变量 int32bit,其值为对 int 执行 2的32次幂 按模运算
  • 如果 int32bit >= 2的31次幂,返回 int32bit - 2的32次幂;否则返回 int32bit

ToUint32 ( argument )

argument 转成32位无符号的整数,取值范围在 0 ~ Math.pow(2, 32) - 1 之间。

  • 前 4 步与 ToInt32 一致
  • 最后直接返回 int32bit

ToInt16 ( argument )

转为16位(2字节)有符号整数,取值范围在 -32768 ~ 32767

  • 前三步与 ToInt32 一致
  • 定义过程变量 int16bit,其值为对 int 执行 2的16次幂 按模运算
  • 如果 int32bit >= 2的15次幂,返回 int16bit - 2的16次幂;否则返回 int16bit

ToUint16 ( argument )

转为16位(2字节)无符号整数,取值范围在 0 ~ Math.pow(2, 16) - 1

  • 前四步与 ToInt16 一致
  • 最后直接返回 int16bit

ToInt8 ( argument )

转为 8 位有符号整数,取值范围在 -128 ~ 127,即 Math.pow(2, -7) ~ Math.pow(2, 7) - 1

  • 前三步与 ToInt16 一致
  • 定义过程变量 int8bit,其值为对 int 执行 2的8次幂 按模运算
  • 如果 int8bit >= 2的7次幂,返回 int8bit - 2的8次幂;否则返回 int8bit

ToUint8 ( argument )

转为 8 位无符号整数,取值范围在 0 ~ 255,即 0 ~ Math.pow(2, 8) - 1

  • 前四步与 ToInt8 一致
  • 最后直接返回 int8bit

ToUint8Clamp ( argument )

目的和 ToUint8 一样,但是过程不一样

  • 定义过程变量 number,将 ToNumber(argument) 结果赋值给 number
  • 如果 numberNaN,返回 +0
  • 如果 number <= 0, 返回 +0
  • 如果 number >= 255, 返回 255
  • 如果不符合以上条件,定义过程变量 f,对 number 四舍五入并赋值给 f
  • 如果 f + 0.5 < number,返回 f + 1
  • 如果 f + 0.5 > number, 返回 f
  • 如果 f 是奇书,返回 f + 1
  • 都不满足,返回 f

ToUint8Clamp 和 Math.round 不同,ToUint8Clamp 舍入到一半,但是 Math.round 是四舍五入。

ToBigInt ( argument )

  • 定义过程变量 prim,执行 ToPrimitive(argument, hint Number) 将结果赋值给 prim
  • prim 值对照下表返回
参数类型 结果
Undefined 返回类型错误
Null 返回类型错误
Boolean true => 1n,false => 0n
BigInt 返回prim
Number 返回类型错误
String 执行StringToBigInt(prim),如果结果是NaN,返回类型错误,否则返回结果
Symbol 返回类型错误

其实这里对 Number 类型的参数执行结果和上面的 ToNumber 一样令人困惑,但这里的本意是指 禁止我们开发中将Number隐式转换为BigInt,要想转换,必须显式调用

根据 重学js —— js数据类型:BigInt 对象 一章,BigInt(Number 类型值) 会做特殊处理,不会走 ToBigInt 方法

StringToBigInt ( argument )

调用 https://tc39.es/ecma262/#sec-tonumber-applied-to-the-string-type 处的算法,略微有点不同:

  • DecimalDigits替换StrUnsignedDecimalLiteral,不允许无穷大,小数点或指数。
  • 如果数学值是 NaN,返回 NaN;否则,返回完全对应于数学值的BigInt,而不是四舍五入为数字。

ToBigInt64 ( argument )

转换为64位有符号整数值,取值范围 Math.pow(2, -63) ~ Math.pow(2, 63) - 1

  • 定义过程变量 n,调用 ToBigInt(argument) 将结果赋值给 n
  • 定义过程变量 int64bit,对 n 进行2的64次幂 取模运算,将结果赋值给 int64bit
  • 如果 int64bit >= Math.pow(2, 63),返回 int64bit - Math.pow(2, 64);否则返回 int64bit

ToBigUint64 ( argument )

转换为64位无符号整数值,取值范围 0 ~ Math.pow(2, 64) - 1

  • 前两步与 ToBigInt64 一致
  • 返回 int64bit

ToString ( argument )

见表:

参数类型 结果
Undefined "undefined"
Null "null"
Boolean true => "true",false => "false"
Number 返回! Number::toString(argument)
String 返回 argument
Symbol 类型错误
BigInt 返回! BigInt::toString(argument)
Object 执行ToPrimitive(argument, hint String) 将值赋给 primValue ,再返回 ToString(primValue)

这里 Symbol 转为 String,调用 toString() 或者 String() 来转换是OK的,但是直接使用 "" + Symbol() 会报错,本意应该还是希望我们进行显式转换。

ToObject ( argument )

见下表:

参数类型 结果
Undefined 类型错误
Null 类型错误
Boolean 返回一个Boolean对象,其[[BooleanData]]内置插槽值为argument,详见Boolean Objects
Number 返回一个Number对象,其[[NumberData]]内置插槽值为argument,详见Number Objects
String 返回一个String对象,其[[StringData]]内置插槽值为argument,详见String Objects
Symbol 返回一个Symbol对象,其[[SymbolData]]内置插槽值为argument,详见Symbol Objects
BigInt 返回一个BigInt对象,其[[BigIntData]]内置插槽值为argument,详见BigInt Objects
Object 返回argument

ToPropertyKey ( argument )

听名字就知道,转换成对象能用的属性类型:String 和 Symbol

ToLength ( argument )

给类数组对象用的,确定其长度

  • 先执行 ToInteger(argument) 并赋值给 len
  • 如果 len <= +0,返回 +0
  • 否则返回 len 与 Math.pow(2, 53) 两者中较小的数

CanonicalNumericIndexString ( argument )

如果是ToString会生成的Number的字符串表示形式,则返回转换为数值的参数,或者是字符串 "-0"。否则返回 undefined

有点绕人,就是看argument能不能转换成 Number,不能返回 undefined,能得话区分 -0

ToIndex ( value )

如果是有效的整数索引值,则将其值参数转换为非负整数。

回归代码

回到开篇给出的那些代码,结合规范,其中一些代码能直接给出原因,例如:

+true // 1
+ false // 0
+null // 0
+undefined // NaN
Boolean(NaN) // false
Boolean(Infinity) // true
Boolean(-Infinity) // true
Boolean(Symbol()) // true
+Symbol() // 报错
''+Symbol() // 报错
+'-0' // -0 这里看上文的String转Number
+'Infinity' // Infinity 这里看上文的String转Number
+'-Infinity' // -Infinity 这里看上文的String转Number

但是还有很多代码,仍需要深究为什么?

例如以下代码,需要看Number::toString(argument)

''+(-0) // "0"
''+(-Infinity) // "-Infinity"

Number::toString(argument)中规定无论 +0 还是 -0,都返回 "0"小于0的argument会返回带-的字符串"-argument"。

还有与对象相关的转换为 NumberString

+{} // NaN
+[] // 0
+[1] // 1
+[Infinity] // Infinity
+[-0] // 0
+ [1, 2] // NaN
+[{}] // NaN
''+[] // ""
''+[1] // "1"
''+[1,2,3] // "1,2,3"
''+[Infinity, Infinity] // "Infinity,Infinity"
''+{} // "[object Object]"
''+function test (){} // "function test (){}"

上面不论是转为 Number 还是 String,其本质都需要调用 ToPrimitive 算法,问题的关键点就在该算法内部调用的OrdinaryToPrimitive算法的规则。其实本质上就是调用对象的 valurOf()toString()

实现个ToPrimitive

const ToPrimitive = (input, PreferredType) => {
  if (typeof input !== 'object' && typeof input !== "symbol") {
    return input;
  }
  let hint;
  if (!PreferredType) {
    hint = "defaullt";
  } else {
    hint = typeof PreferredType === 'String' ? "string" : "number";
  }
 if (typeof input === "symbol") {
    let exoticToPrim = Symbol.prototype[Symbol.toPrimitive];
    if (exoticToPrim !== undefined) {
      let result = exoticToPrim.call(input);
      if (typeof result !== 'object') {
        return result;
      } else {
        throw "TypeError";
      }
    }
  }
  if (hint === "defaullt") {
    hint = "number";
  }
  if (hint === "number") {
    if (typeof input.valueOf() !== 'object') {
      return input.valueOf();
    }
    if (typeof input.toString() !== 'object') {
      return input.toString();
    }
  } else {
    if (typeof input.toString() !== 'object') {
      return input.toString();
    }
    if (typeof input.valueOf() !== 'object') {
      return input.valueOf();
    }
  }
  throw "TypeError";
}

参考

  1. Js中的对象转换为数值类型时是不是返回结果都是NaN?
  2. JS那些巧妙的数据类型转换--实用版

2020-07-29 补充

数组类型转换时犯浑了!

[1, 2] + [2, 1] // 1,22,1

我误以为数组和对象的 toString 返回一致,其实我记错了,原题啊都能记错!!!

({}).toString() // "[object Object]"

我TM看到 [1, 2] 这种心想怎么转为数值啊?是不是转为 "[object Array]" 啊???
其实数组转为原始值,存在多个元素的话会通过类似 join(',') 的操作转为字符串的!!!

重学js——执行上下文(Execution Contexts)

Execution Contexts

执行上下文是一种规范机制,用于跟踪对代码的运行时求值,通过ECMAScript的某种实现。在任何时间点,每个实际执行代码的代理最多有一个执行上下文。这就是代理运行时执行上下文

执行上下文栈用于跟踪执行上下文。运行时执行上下文总是位于栈的顶部。 当控制权从与当前运行时执行上下文关联的可执行代码转移到与执行上下文无关的可执行代码时,会创建新的执行上下文(可以认为,每个可执行代码都有与之关联的执行上下文)。最新被创建的执行上下文会被push到栈顶,成为运行时执行上下文。

每个执行上下文至少有下表中列出的状态组成:

组成部分 意义
code evaluation state 执行、挂起和恢复与此执行上下文关联的代码计算所需的任何状态。
Function 如果执行上下文正在计算函数对象,那么该 Function 的值就是那个函数对象。如果上下文正在计算脚本或者,那么 Function 的值就是 null
Realm(领域) 关联代码从中访问ECMAScript资源的领域记录。
ScriptOrModule 关联代码的模块记录或者脚本记录。如果没有原始脚本或模块,那么与在InitializeHostDefinedRealm中创建的原始执行上下文一样,该 ScriptOrModule值为 null

伪代码大致是这样的:

// 执行上下文
EC = [
  state
  fun
  realm
  null
];
// 执行上下文栈。EC1,EC2,EC3都是伪代码模拟执行上下文
ECStack = [
  EC1,
  EC2
  ...
];
// 后进先出
ECStack.push(EC3) // EC3放到栈顶
ECStack.pop() // EC3先执行完出来

一旦运行时执行上下文被挂起,另一个不同的执行上下文会成为新的 运行时执行上下文 并且开始执行属于它的代码。以后某个时段,被挂起的执行上下文可能再次成为运行时执行上下文并且在它之前被挂起的地方继续计算它的代码。运行时执行上下文状态在执行上下文之间的转换通常以类似于后进先出的栈方式进行。然而,一些ECMAScript特性需要运行时执行上下文的非后进先出转换。

活跃的函数对象

运行时执行上下文领域部分的值也被称为当前领域记录(the current Realm Record)。运行时执行上下文的 Function 部分的值也被叫做 活跃的函数对象(the active function object)

ECMAScript代码的执行上下文还有额外的状态组成部分,看下表:

组成部分 意义
LexicalEnvironment 词法环境的标识符,用于解析在此执行上下文中由代码生成的标识符引用。
VariableEnvironment 词法环境的标识符,其EnvironmentRecord保存此执行上下文中由VariableStatement创建的绑定。

注意:LexicalEnvironment和VariableEnvironment全都是词法环境。

表示生成器对象求值的执行上下文有额外的状态部分,看下表:

组成部分 意义
Generator 此执行上下文正在计算的GeneratorObject

执行上下文纯粹是一种规范机制,不需要与ECMAScript实现的任何特定工件相对应。ECMAScript代码是无法直接获取或者观察到执行上下文的。

ResolveBinding(name [ , env ])

该抽象运算用来决定传入的字符串 name 绑定。可选参数 env 明确提供用于绑定的 词法环境

  1. 如果 env 不存在,或为 undefined
    1. env 设为 运行时执行上下文词法环境
  2. 断言:env词法环境
  3. 如果严格模式代码中包含与要求值的语法产生相匹配的代码,定义 stricttrue,否则为 false
  4. 返回 ? GetIdentifierReference(env, name, strict)

GetThisEnvironment()

该抽象运算会查找用于当前提供给 this 关键字绑定的 环境记录

  1. 定义 lex运行时执行上下文 的词法环境
  2. 重复,
  3. 定义 envReclex环境记录
  4. 定义 existsenvRec.HasThisBinding()
  5. 如果 existstrue,返回 envRec
  6. 定义 outerlex 的外部环境引用值
  7. 断言:outernull
  8. lex 赋值给 outer

注意:步骤2中的循环最终会终止,因为 环境列表始终以具有 this 绑定的 全局环境 结尾

ResolveThisBinding()

  1. 定义 envRecGetThisEnvironment()
  2. 返回 ? envRec.GetThisBinding()

拓展阅读

JavaScript深入之执行上下文栈——冴羽

注意

  1. 在网上看到一些文章说 执行上下文总共有三种类型?,这一点我在规范中没有找到,暂时不知为何他们会得出这个结论。
  2. 个人感觉,拓展阅读里的最后思考题,第二个函数执行上下文入栈出栈应该是这样的:
ECStack.push(<checkscope> functionContext);
ECStack.pop();
ECStack.push(<checkscope> functionContext);
ECStack.push(<f> functionContext);
ECStack.pop();
ECStack.pop();
// 函数f涉及到闭包,它的词法环境会有对checkscope环境的引用,它的直接父级始终是checkscope。

数据结构、算法相关收藏

  1. VisuAlgo
  2. 算法导论视频
  3. 可以买本《数据结构与算法 JavaScript描述》,适合入门
  4. 有一定实力的可以去刷牛客网和leetcode
  5. JavaScript 算法与数据结构
  6. LeetCodeAnimation
    动画版

自己写的相关记录

  1. 读《数据结构与算法 JavaScript描述》——实现一个栈结构
  2. 读《数据结构与算法 JavaScript描述》——实现一个队列结构
  3. 读《数据结构与算法 JavaScript描述》——实现一个链表结构

排序

冒泡与选择排序 2020-08-06

以前我只会冒泡和快排,今天知道了 选择,一时没绕过来,都不知道它和冒泡有什么区别,研究了一段时间才发现:

选择排序,也用到了嵌套 for 循环,不过它会在每次遍历时先保存当前循环的第一个数 arr[i],假设它为当前循环中最小的 min,然后用后面的数与 min 比较,如果比 min 还小,则改变 min 的值为这个更小的数,一直到内循环结束,再通过 arr[i] = min 即可将当前内循环最小的数移动到当前循环开始的索引位置。

let arr = [9,8,5,9,6,7,19,2,1]
for (let i = 0; i < arr.length - 1; i ++) {
  let min = arr[i]
  for (let j = i + 1; j < arr.length; j++) {
    if (arr[j] < min) {
      let temp = arr[j]
      arr[j] = min
      min = temp
    }
  }
  arr[i] = min
}

插入排序 2020-08-07

今天又学到了个 插入排序,还是嵌套循环,看书上定义一时没明白什么意思,有点迷糊,看看代码手动去敲一遍就容易理解了。实话说这三个排序还是 冒泡 最易懂,可能因为接触的早吧。

for (let i = 1; i < arr.length; i++) {
  let temp = arr[i]
  let j = i
  while(j > 0 && arr[j - 1] > temp) {
    arr[j] = arr[j - 1]
    j--
  }
  arr[j] = temp
}

注意:最后是 arr[j] = temp,用的是内循环的 j 而不是外循环的 i

注意:插入排序 外循环是从索引 1 开始的

希尔排序 2020-08-7

这个排序搞了我半天!真不容易懂啊。属于 插入排序 的变种升级,普通 插入排序 都是一个接一个顺序遍历的,希尔排序 感觉是先选好开始遍历的 起始位置(间隔序列里面的元素),然后从起始位置遍历到数组末尾。
重点是内部第三层循环,为何是 j >= gaps[g] 还有 j - gaps[g]?说白了为何与第一层循环确定的间隔比而不是与第二层循环确定的索引比?

其实第一层循环确定的是 间隔 即遍历 起始位置,内部嵌套的两层循环都应该与这个 起始位置 比较。如果第三层循环直接与第二层自增的索引比较,那将毫无意义,最多当 j === i 时才会满足,无法确定连续元素的大小关系。而与第一层的 起始位置 对比,第三层循环通过 j = j - gaps[g] 可以确定 起始位置 后面的连续元素。

let gaps = [5, 3, 1] // 间隔序列
let arr = [9,8,5,9,6,7,19,2,1]

// 遍历间隔序列
for (let g = 0; g < gaps.length; g++) {
  // 根据 间隔 确定遍历起始位置,一直到数组最后一个
  for (let i = gaps[g]; i < arr.length; i++) {
    let temp = arr[i]
    let j
    for (j = i; j >= gaps[g] && arr[j - gaps[g]] > temp; j = j - gaps[g]) {
      arr[j] = arr[j - gaps[g]]
    }
    arr[j] = temp
  }
}

归并排序 2020-08-08

《数据结构与算法JavaScript描述》中提到 归并排序 分为 自顶向下(需要递归)(大多数情况) 和 自底向上 两种,书中用的是 自底向上,书中的看不进去,在 十大经典排序算法动画与解析,看我就够了!(配代码完全版) 找到了 自顶向下 版本代码,看了动画想自己去实现,无从下笔,只能看他代码实现了,看着看着自然就恍然大悟了,这里给出js版:

let arr = [9,8,5,9,6,7,19,2,1]

const slice = (arr) => {
  if (arr.length < 2) {
    return arr
  }
  let middle = Math.floor(arr.length / 2)
  let left = arr.slice(0, middle) // 注意:这里不能 middle + 1,否则栈溢出
  let right = arr.slice(middle)
  return merge(slice(left), slice(right))
}

const merge = (left, right) => {
  let result = []
  while (left.length > 0 && right.length > 0) {
    if (left[0] < right[0]) {
      result.push(left[0])
      left = left.slice(1)
    } else {
      result.push(right[0])
      right = right.slice(1)
    }
  }

  while (left.length > 0) {
    result.push(left[0])
    left = left.slice(1)
  }
  while (right.length > 0) {
    result.push(right[0])
    right = right.slice(1)   
  }

  return result
}

我在一开始编写时,slice 里面代码是这样的

left = arr.slice(0, middle + 1)

但是测试时直接报 栈溢出。一开始很是费解,直到我看到这里,想到如果 arr 一开始是 [3, 2] 这种只有两个元素的数组,那么传进去程序将会一直陷入对 left 的无限求值,并且 left 值始终维持在 [3, 2] 不会改变!
left = arr.slice(0, middle) 则会避免这种情况,无论 middle 是奇数还是偶数,都能保证递归到最底层时只有一个元素!

这是细节问题,我自己想优化下写法结果打脸了,这样也好,能加深理解。

读《你不知道的javascript》中卷—类型与语法

前言

断断续续读《你不知道的javascript》也很久了,人还是太懒了,自控力太弱了。

数组

1.使用delete运算符可以将单元从数组中删除,但是!数组的length属性不变!!!
2.数组也是对象,所以可以包含键值和属性,但是他们不计算在数组长度内。

var a = [];
a[0] = 1;
a['foobar'] = 2;
a.length; // 1
a['foobar']; // 2
a.foobar; // 2

3.特别注意!如果字符串键值能够被强制转为十进制数字的话,它会被当做数字索引来处理!

var a = [];
a["13"] = 42;
a.length; // 14

4.slice()可以实现数组浅拷贝。注意:如果数组的值均是基本数据类型,那么此时slice实现的是深拷贝。一旦包含引用数据类型,就是浅拷贝。

数字

1..运算符需要给与特别注意,因为它是一个有效的数字字符,会被优先识别为数字常量的一部分,然后才是对象属性访问运算符。

(42).toFixed(3); // '42.000'
0.42.toFixed(3); // '0.420'
42..toFixed(3); // '42.000'

数值

1.void并不改变表达式的结果,只是让表达式不返回值。

var a = 42;
console.log(void a, a); // undefined, 42

2.NaN是一个特殊值,它和自身不相等。是一个非自反的值。

NaN === NaN; // false
NaN !== NaN; // true

可以用isNaN()来判断一个值是否是NaN,但是结果并不准确。

var a = 2 / 'foo';
var b = 'foo';
window.isNaN(a); // true
window.isNaN(b); // true

es6提供了Number.isNaN()来进行判断

Number.isNaN(a); // true
Number.isNaN(b); // false

3.-0相关

  • 为什么需要-0?
    • “-”代表值为0的变量的符号位,表示它的方向信息。
  • 转换与比较
    0 === -0; // true
    var a = 0 / -3; // -0
    a.toString(); // "0"
    a + ""; // "0"
    JSON.stringify(a); // "0"
    +"-0"; // -0
    Number("-0"); // -0
    JSON.parse("-0"); // -0

4.Object.is()
用于判断两个值是否绝对相等。

var a = 2/"foo"; // NaN
Object.is(a, NaN); // true

原生函数

  • String()
  • Number()
  • Boolean()
  • Array()
  • Object()
  • Function()
  • RegExp()
  • Date()
  • Error()
  • Symbol()

1.内部属性[[Class]]

所有typeof返回值为"object"的对象都包含一个内部属性[[Class]]。一般通过Object.prototype.toString()来查看。根据ES6的规范,[[Class]]属性改成了internal slot

Object.prototype.toString.call(null); // "[object Null]"

2.封装对象

var a = new Boolean(false); // 此时a是一个对象,chrome控制台打印:Boolean{false}
a.valueOf(); // false;利用valueOf()进行拆封

3.空数组问题

var a = []
a.length = 3
a // [empty x 3]
a[0] === undefined // true
a.indexOf(undefined) // -1

上述示例表明,虽然数组a长度为3且每个元素值等于undefined,但是其实它内部并没有undefined!!!
如何创建所有值都是undefined的数组呢?

var a = Array.apply(null, {length: 3})
a // [undefined, undefined, undefined]

强制类型转换

1.JSON.stringify()在对象中遇到undefinedfunctionsymbol时会自动将其忽略,在数组中则会返回null。(以保证单元位置不变)

2.对包含循环引用的对象执行JSON.stringify()会出错。

3.如果对象中定义了toJSON()方法,JSON字符串化时会首先调用该方法,然后用它的返回值来进行序列化。如果要对有非法JSON值的对象做字符串化,或者对象中的某些值无法被序列化时,就需要定义toJSON()方法来返回一个安全的JSON值。

4.可以向JSON.stringify()传递一个可选参数replacer,它可以是数组或函数,用来指定对象序列化过程中哪些属性应该被处理,哪些应该被排除,和toJSON()很像。

  • 如果replacer是一个数组,那么它必须是一个字符串数组,其中包含序列化要处理的对象的属性名称,除此之外其他的属性则被忽略
  • 如果replacer是一个函数,它会对对象本身调用一次,然后对对象中的每个属性各调用一次,每次传递两个参数,键和值。如果要忽略某个键就返回undefined,否则返回指定的值。
var a = {
  b: 42,
  c: '42',
  d: [1, 2, 3]
}
JSON.stringify(a, ["b", "c"]) // "{"b": 42, "c": "42"}"
JSON.stringify(a, function (k, v) {
  if (k !== "c") {
    return v
  }
}) // "{"b": 42, "d": [1, 2, 3]}"

5.为了将值转换为相应的基本类型值,抽象操作ToPrimitive会首先检查该值是否有valueOf()方法。如果有并且返回基本类型值,就使用该值进行强制类型转换。如果没有就使用toString()的返回值来进行强制类型转换。如果valueOf()toString()均不返回基本类型值,会产生TypeError

6.假值和真值

var a = new Boolean(false)
var b = new Number(0)
var c = new String("")
a && b && c // String{"", length: 0}
Boolean([]) // true
Boolean({}) // true
Boolean(funtion(){}) // true

重学js

跟着规范和MDN重学js

该系列知识来自 ECMAScript 2020 Language 规范以及 MDN JavaScript

注意:本系列主要是ECMAScript规范,而 setTimeout/setInterval/atob 等属于 HTML规范,看 WindowOrWorkerGlobalScope

ECMAScript新提案看这里:https://github.com/tc39/proposals

找到个 ECMAScript5.1中文版——颜海镜

推荐:

  1. 《JavaScript 20 年》中文版
  2. 现代 JavaScript 教程
  3. Web 开发技术
  4. You-Dont-Know-JS
  5. 冴羽的博客
  6. javascript-questions
  7. Daily-Interview-Question

为什么要重学js?

因为我的js基础实在太差了!!!

直接原因:花钱买了winter的《重学前端》,结果关于js的系列看了几篇后十分懵逼,感觉我学的js和他学的不是同一个,再往下看一堆不懂得知识,实在没法子了,只能先结合规范和MDN重学下,然后穿插着看《重学前端》

我不是要做到熟记每个js api,因为我不能保证每个api都经常使用。我要做的是跟着规范和MDN将js系统性、全面性的学习,即使很多不常用的api也能混个脸熟,不至于一脸茫然;对于js中隐藏的很多坑点能有清晰地认知。

比如以下代码:

+[] // 0
![] // false
{} + [] // 0
[] + {} // '[object Object]'

parseInt('7+6', 10) // 7
parseFloat('') // NaN
Number('') // 0

这是为何?尤其是 parseFloatNumber 对空字符串的求值为何如此不同?

当我想知道为什么时,我只能去网上搜索,这恰恰证明了基础的薄弱!即使搜索到答案,也只是很碎片化的,未来又冒出一些新的坑点怎么办呢???

即使从一些文章中获取到一些零碎的知识,但是这些知识一定正确吗???谁能保证???

所以,重学js必须要进行!

只看MDN不行吗?

当然可以。MDN很棒!

但是,正如上面我列出的代码 +[] // 0,我并没有从MDN上找到,可能是我姿势不对,但不可否认,MDN就像api字典,如果都不知道具体要查哪个关键字,那么MDN的好处就得不到体现!

最关键的是,MDN给我感觉知识点有点散,比如函数相关,分散在好多地方,如果配合规范看完然后整合下,形成一个系统全面顺序进行的js学习系列可能更好!

学习之前,需要了解一些常识:

学习目录:

  1. 词法文法
  2. js规范中对token的定义(第二段话第一句)
    • 除空白和注释之外的输入元素构成ECMAScript语法的终端符号,称为ECMAScript标记(tokens)。
    • 这些标记(token)是ECMAScript语言的保留字、标识符、字面量和标点符号。
    • Tokens (CommonToken)
    • 很多文章喜欢把token翻译成 ,或许这样更容易理解吧。
  3. 重学js —— 算法约定(阅读规范需要)
  4. 重学js —— js数据类型:Null、Undefined、Boolean
  5. 重学js —— js数据类型:Number(一)
  6. 重学js —— js数据类型:Number(二)
  7. 重学js —— js数据类型:Number 对象
  8. 重学js —— js数据类型:Number 原型对象和实例上的属性(tofixed/toLocaleString/toPrecision/toString等)
  9. 重学js —— js数据类型:String
  10. 重学js —— js数据类型:文本处理——String对象一:构造器上的属性
  11. 重学js —— js数据类型:文本处理——String对象二:原型对象上的属性(一)
  12. 重学js —— js数据类型:Symbol
  13. 重学js —— js数据类型:BigInt
  14. 重学js —— js数据类型:BigInt 对象(asIntN/asUintN/tolocalestring/tostring)
  15. 重学js —— js数据类型:Object 基础介绍
  16. 重学js —— 众所周知的固有对象
  17. 重学js —— ECMAScript Specification Types(规范类型)
  18. 重学js —— Lexical Environments(词法环境)和 Environment Records(环境记录)(作用域)
  19. 重学js —— 执行上下文(Execution Contexts)
  20. 重学js —— ES规范定义的任务和任务队列(Jobs and Job Queues)
  21. 重学js —— 规范中的Agents阅读
  22. 重学js —— 类型转换
  23. 重学js —— 比较
  24. 重学js —— Operations on Objects and Operations on Iterator Objects
  25. 重学js —— 普通对象和奇异对象行为(普通对象内部方法和内置插槽)
  26. 重学js —— 函数对象
  27. 重学js —— 内置函数对象
  28. 重学js —— Proxy(代理)对象的内置方法和内置插槽
  29. 重学js —— 源文本
  30. 重学js —— 左侧表达式(new/super/import()/可选链等)
  31. 重学js —— 更新表达式(后缀++/前缀++)
  32. 重学js —— 一元运算符(delete/void/typeof/+/-/~/!)
  33. 重学js —— 求幂运算,乘法运算符,加法运算符以及减法运算符
  34. 重学js —— 按位移位运算符
  35. 重学js —— 关系运算符(我只列出instanceof和in)和相等运算符
  36. 重学js —— 二进制按位运算符和二进制逻辑运算符以及条件运算符
  37. 重学js —— 赋值运算符(包括解构赋值)和 逗号运算符
  38. 重学js —— 主要表达式一(this等)
  39. 重学js —— 主要表达式二
  40. 重学js —— 语句和声明
  41. 重学js —— 块语句、声明和变量语句、空语句以及表达式语句
  42. 重学js —— 函数:函数定义
  43. 重学js —— 函数:函数块级作用域坑点(一道题目引发的思考)
  44. 重学js —— 函数:箭头函数定义和方法定义(包含get/set)
  45. 重学js —— 迭代协议(MDN)
  46. 重学js —— 迭代器和生成器(MDN)
  47. 重学js —— 函数:Generator函数定义
  48. 重学js —— 函数:异步函数定义
  49. 重学js —— 函数:异步 Generator 函数定义和异步箭头函数定义
  50. 重学js —— 函数:默认参数值(MDN)
  51. 重学js —— 函数:剩余参数(MDN)
  52. 重学js —— 函数:Arguments 对象(MDN)
  53. 重学js —— 函数:闭包(MDN)
  54. 重学js —— 函数:闭包(翻译自stackoverflow)
  55. 重学js —— 函数:IIFE(立即调用函数表达式)(MDN)
  56. 重学js —— 函数:尾调用
  57. 重学js —— 继承与原型链
  58. 重学js —— 类定义(class)
  59. 重学js —— ECMAScript 标准内置对象(不列出所有内置对象)
  60. 重学js —— 全局对象:值属性与函数属性(globalThis/Infinity/NaN/undefined/eval/isFinite/isNaN)
  61. 重学js —— 全局对象:函数属性(parseFloat/parseInt)
  62. 重学js —— 全局对象:函数属性(URI处理函数)
  63. 重学js —— 全局对象:构造器属性和其它属性
  64. 重学js —— 基本对象:普通对象(Object构造器上的属性一)
  65. 重学js —— 基本对象:普通对象(Object构造器上的属性二)
  66. 重学js —— 基本对象:普通对象(原型对象属性和对象实例属性)
  67. 重学js —— 基本对象:函数对象(apply/bind/call)
  68. 重学js —— 基本对象:布尔对象
  69. 重学js —— 基本对象:Error对象
  70. 重学js —— Math 对象
  71. 重学js —— Date 对象一
  72. 重学js —— Date 对象二:构造器和原型对象上的属性
  73. 重学js —— 索引集合之数组对象
  74. 重学js —— 索引集合之Array原型对象属性
  75. 重学js —— 索引集合之TypedArray 对象(类型化数组)
  76. 重学js —— 结构化数据之ArrayBuffer对象
  77. 重学js —— 结构化数据之SharedArrayBuffer对象
  78. 重学js —— 结构化数据之DataView对象
  79. 重学js —— 结构化数据之Atomics对象
  80. 重学js —— 结构化数据之JSON对象
  81. 重学js —— 键集合之Map对象
  82. 重学js —— 键集合之Set对象
  83. 重学js —— 键集合之WeakMap对象和WeakSet对象
  84. 重学js —— 控制抽象对象之Iteration(迭代)和所有Generator对象
  85. 重学js —— 控制抽象对象之Promise
  86. 重学js —— 控制抽象对象之AsyncFunction对象
  87. 重学js —— Reflect对象
  88. 重学js —— Scripts
  89. 重学js —— modules:循环模块记录
  90. 重学js —— modules:源文本模块记录
  91. 重学js —— modules:内部部分算法
  92. 重学js —— Tree shaking (MDN)
  93. 重学js —— modules:Imports
  94. 重学js —— modules:Exports
  95. 重学js —— 内存模型
  96. 重学js —— JavaScript 错误参考 (MDN)

引用

  1. 系列文中有部分示例代码来源于 高级前端面试小程序,该项目的作者也是 Daily-Interview-Question 作者
  2. 部分知识结合 winter 《重学前端》系列更容易理解,需要在极客时间上购买。(PS:我就是因为买了看不懂才写这个系列的。。。)

锻炼

  1. 能手写 call/apply/bind/new/instanceof 等原生API的模拟实现
  2. 30-seconds-of-code
  3. 手写 防抖/节流 等常用业务工具方法,可见 lodashunderscore
  4. 使用defineProperty监听数组改变(然后再用proxy实现下),我的示例
  5. 将第4条与发布订阅模式结合:HcySunYang/observer-dep-watch
  6. 实现个 mvvm
  7. 尝试实现 虚拟DOM
  8. 第6条与第7条相结合,即demo版 vue
  9. 看看 react 与 vue 源码,学习react源码的实现

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.