推荐一本书《javascript 忍者秘籍2》

2020/01/03 书籍推荐 共 8238 字,约 24 分钟

为什么要推荐《javascript 忍者秘籍2》

本文写于2019年01月30日,从我的掘金迁移过来

我要推荐一本书,《javascript 忍者秘籍2》文章里面所有的图片内容都来自书籍中。我建议你去读读这本书

JavaScript 应用能在很多环境中执行。但是 JavaScript 最初的运行环境是浏览器环境,而其他运行环境也是借鉴于浏览器环境。

我们需要了解javaScript 工作核心原理和浏览器提供的核心 api

我们所接触的大部分东西都有他的生命周期,比我们前端三大框架中 vue 和 react

任何一个东西都有一个从开始到结束的过程。我们的前端页面也是一样的,只是我们平时忽略他罢了。当我们在浏览器地址栏里面输入一串 url 开始他的生命周期就已经开始了,当我们关闭网页的时候他的生命周期就结束了。如图所示

作为用户我们所关注的是页面的构建和事件的处理

页面构建又可以分为解析 HTML 代码并且构建文档对象模型 DOM 和执行 JavaScript 代码

注意了 DOM 是根据 HTML 代码来创建的,但是两者并不是相同的。我们可以把 HTML 代码看作浏览器页面 UI 构建初始 DOM 的蓝图。为了正确构建每个 DOM,浏览器还会修复它在蓝图中发现的问题。比如在 p 元素里面包裹 div 元素,最终渲染的并不是父子关系,而是兄弟关系(可以自己尝试一下)

当解析到脚本元素时,浏览器就会停止从 HTML 构建 DOM,并开始执行 JavaScript 代码。为了避免解析 JavaScript 代码花费太长时间,而阻塞页面渲染。我们都是建议把JavaScript 代码放到 body 元素后面.或者在 script 标签上面加上 defer async 让 JavaScript 代码和 DOM 构建同步

浏览器暴露给 JavaScript 引擎的主要全局对象是 window 对象,它代表了包含着一个页面的窗口。 window 对象是获取所有其他全局对象、全局变量(甚至包含用户定义对象)和浏览器 API 的访问途径。全局 window 对象最重要的属性是 document,它代表了当前页面的 DOM。

包含在函数内的代码叫作函数代码,而在所有函数以外的代码叫作全局代码。 执行上下文也分两种 全局执行上下文和函数执行上下文;当 JavaScript 程序开始执行时就已经创建了全局上下文;而函数执行上下文是在每次调用函数时,就会创建一个新的

页面构建完了之后变进入第二个阶段,事件处理

浏览器执行环境的核心思想基于:同一时刻只能执行一个代码片段,即所谓的单线程执行模型。采用事件队列来跟踪发生但是尚未执行的事件

函数具体介绍请看这

JavaScript 解析器必须能够轻易区分函数声明和函数表达式之间的区别。如果去掉包裹函数表达式的括号,把立即调用作为一个独立语句 function() {}(3),JavaScript 开始解析时便会结束,因为这个独立语句以 function 开头,那么解析器就会认为它在处理一个函数声明。每个函数声明必须有一个名字(然而这里并没有指定名字),所以程序执行到这里会报错

问: var samurai = (() => "Tomoe")(); 和 var ninja = (() => {"Yoshi"})(); 分别返回什么?

函数具有属性,而且这些属性能够被存储任何信息,我们可以利用这个特性来做很多事情;例如:

  //储存函数,利用函数具有属性,而且这些属性能够被存储任何信息
  let store = {
    nextId:1,
    cache:{},
    add(fn){
      if(!fn.id && typeof fn =='function'){
        fn.id=this.nextId++
        this.cache[fn.id]=fn
        return true
      }
    }
  }
  //记忆函数
  function isPrime(value){
    if(!isPrime.answers){
      isPrime.answers = {}
    }
    if(!isPrime.answers[value]){
      console.log(1)
      return isPrime.answers[value] = value
    }
    console.log(3)
    return isPrime.answers[value]
  }
  isPrime(2)

Number、String 和 Boolean,三个构造器是两用的,当跟 new 搭配时,它们产生对象,当直接调用时,它们表示强制类型转换。

记忆函数可以做什么???

我们可以把它理解成闭包,他可以记住之前请求的结果,比如需要全局使用的某一个变量,而且不会经常变化的量,我们就可以用这个来做。如果他已经存在了我们就不做任何处理,没有存在,我们就去获取他的存在(一个的网页用户信息,

我们在给函数传参数的时候,除了有我们显示传入的实参之外,其实还包含了两个隐士参数 this 和 arguments。this 表示被调用函数的上下文(在什么环境下调用,就指向什么)。arguments 表示函数调用过程中传递的所有参数

arguments 是伪数组,在 es6 中有一个剩余参数的概念,剩余参数是一个真正的数组

特例: 箭头函数的 this 与声明所在的上下文的相同,无论何时在哪调用,只和声明的地方有关系(定义时的函数继承上下文)

闭包:允许函数访问并操作函数外部的变量,windows 就是一个最大的闭包(回调函数是另一种常见的使用闭包的情景)

promise 模拟一个请求, axios 实现原理,应该就是用他,我并没有阅读过源码我猜测的。

function getJSON(url){
    return new Promise((resolve,reject)=>{//创建并返回一个新的promise对象
        const request = new XMLHttpRequest()//创建一个XMLHttprequest对象
        request.open('GET',url)//初始化请求
        request.onload=function(){//“注册一个onload方法,当服务端的响应后会被调用”
                try{
                if(this.status==200){//“即使服务端正常响应也并不意味着一切如期发生,只有当服务端返回的状态码为200(一切正常)时,再使用服务端的返回结果”
                  resolve(JSON.parse(this.request))//“尝试解析JSON字符串,倘若解析成功则执行resolve,并将解析后的对象作为参数传入”
                }else{
                    reject(this.status+' '+ this.statusText)
                }
            }catch(e){
                reject(e.message)//“如果服务器返回了不同的状态码,或者如果在解析JSON字符串时发生了异常,则对该promise执行reject方法”
                }
        }
        request.onerror=function(){//“如果和服务器端通信过程中发生了错误,则对该promise执行reject方法”
          reject(this.status+' '+ this.statusText)
        }
        request.send()//发送请求
    })
}
getJSON("data/ninjas.json").then(ninjas => {

}).catch(e => fail("Shouldn't be here:" + e));   //←--- 使用由getJSON函数创建的promise来注册resolve和reject回调函数”

处理集合

我们所有的操作都通过代理添加了一个间接层,使我们能够实现所有这些很酷的特性,能够实时检测到任何属性的变化,但与此同时它引入了大量的额外的处理,会影响性能

我们建议谨慎使用代理。尽管使用代理可以创造性地控制对象的访问,但是大量的控制操作将带来性能问题。可以在多性能不敏感的程序里使用代理,但是若多次执行代码时仍然要小心谨慎。像往常一样,我们建议你彻底地测试代码的性能。

使用Map可以创建字典类型,建立键值对的映射关系,在处理特殊的编程任务时这种集合非常有用。而Set集合中的成员都是唯一的,不允许出现重复的成员,Set成员的值都是唯一的,最重要的作用是避免存储多个相同的对象

使用数组字面量创建数组优于数组构造函数。主要原因很简单:[]与new Array()(2个字符与11个字符)。此外,由于JavaScript的高度动态特性,无法阻止修改内置的Array构造函数,也就意味着new Array()创建的不一定是数组。因此,推荐坚持使用数组字面量。

pop和push方法只影响数组最后一个元素:pop移除最后一个元素,push在数组末尾增加元素。shift和unshift方法修改第一个元素,之后的每一个元素的索引都需要调整。因此,pop和push方法比shift和unshift要快很多,非特殊情况不建议使用shift和unshift方法。

映射数组。主要思想是将数组中的每个元素的属性映射到新数组的元素上 => 数组的 map 方法

正则

注意 当正则表达式在开发环境是明确的,推荐优先使用字面量语法;当需要在运行时动态创建字符串来构建正则表达式时,则使用构造函数的方式。

[abc]表示匹配a、b、c中的任意一个字符

[^abc]表示匹配除了a、b、c以外的任意字符

● 指定可选字符(可以出现0次或1次),在字符后添加?,例如,/t? est/可以同时匹配test与est。

● 指定字符必须出现1次或多次,使用+,如/t+est/可匹配test、ttest、tttest等。

● 指定字符出现0次或1次或多次,使用,如/test/匹配test、ttest、tttest以及est。

● 指定重复次数,使用括号指定重复次数,例如/a{4}/,匹配4个连续的字符a。

● 指定循环次数的范围,使用逗号分隔,例如/a{4,10}/匹配4~10个连续的字符a。

● 指定开放区间,省略第2个值,保留逗号。例如/a{4, }/匹配4个或更多个连续的字符a

● 使用竖线()表示或。例如,/ab/可以匹配a或者b, /(ab)+(cd)+/可以匹配一个或多个ab或cd。

● 反向引用分组中捕获的内容,使用反斜线加上数字表示引用,该数字从1开始,第一个分组捕获的为\1,第二个为\2,以此类推 (匹配html的标签 /<(\W+>(.+)<\/\1>/这可以匹配简单的元素如<strong>whatever</strong>),不使用反向引用,我们可以使用$1、$2、$3等标记捕获序号

默认是贪婪模式,可以匹配所有可能的字符。在运算符后添加?,例如a+?,使得运算符为非贪婪模式,只进行最小限度的匹配。

编译阶段发生在正则表达式被创建的时期。执行阶段发生在使用编译之后的正则表达式进行匹配字符串的时期。

在编译过程中,表达式经过JavaScript引擎的解析,转换为内部代码。解析和转换的过程发生在正则表达式创建时期(浏览器会进行内部优化处理工作)。

每次创建一个正则表达式(也被编译)都会创建一个新的正则表达式对象。这与原始类型(如string、number等)不同,因为每个正则对象永远是独一无二的,所以建议我们创建正则表达式的时候用变量缓存起来

使用new RegExp()构造器时,是基于传入的class名称进行编译正则表达式的。这是无法使用正则字面量的场景示例,因为无法提前预知所需查找的class名称。

定义默认捕获的是圆括号里面的内容,如果括号不应该产生捕获,正则表达式语法可以在起始圆括号之后使用符号?:。这就是所谓的被动子表达式(passive subexpression)/(?:sunseekers)/=> 这里就不产生捕获

尽可能在我们的正则表达式中,在不需要捕获的情况下,使用非捕获分组代替捕获,表达式引擎不需要记忆和返回捕获结果,这可以减少很多工作

但是replace最重要的特性是不仅支持替换值,而且支持替换函数作为参数。当第2个参数是函数时,对每一个所匹配到的值都会调用一遍(全局匹配会返回匹配到的全部内容)。

● 全文匹配。

● 匹配时的捕获。

● 在原始字符串匹配的索引。

● 源字符串。

代码模块化

一次又一次的事实证明,小的、组织良好的代码远比庞大的代码更容易理解、更易于维护。因此,很自然,优化程序的结构和组织的方式,就是把它们分成小的、耦合相对松散的片段,这些片段称为模块。

使用对象、闭包和立即执行函数实现模块

洞溪浏览器

HTML字符串转DOM结构用到的是innerHTML属性。能够成功转换需要的条件是: 1.确保HTML字符串是合法有效的。

2.将它包裹在任意符合浏览器规则要求的闭合标签内。例如,<option>元素必须包含在<select>中等。

3.使用innerHTML将这串HTML插入到一个需求DOM中。(插入操作步骤减少到最少,我们经常可以使用DOM片段(DOM fragments)进行插入

4.提取该DOM节点。

当访问元素的特性值时,我们有两种选择:使用传统的DOM方法getAttribute和setAttribute,或使用DOM对象上与之相对应的属性(el.id,el.class)id属性和id特性是以某种方式联系在一起的。修改id属性的值,id特性的值也会跟着改变. 并非所有元素特性都能被属性表示(比如自定义特性

最常用的是style元素属性,它不是字符串,而是一个对象,使用!important 的元素有最高的优先级

获取元素高度的几种方式和区别

offsetHeight = content + border + padding

clientHeight = content + padding

window.getComputedStyle(div).getPropertyValue(‘height’)

注意了,在高度交互网站中,元素的隐藏(display值设置为none时),可能会花一些时间,而且一个元素如果不显示的话,它就没有尺寸。在非显示元素上,尝试获取offsetWidth或offsetHeight属性值,结果都是0。

如果我们想或许隐藏元素的高度的话:将display属性修改为block,可以让我们获取offsetHeight和offsetWidth的真实值,但元素会变成可见。为了使元素不可见,我们将visibility属性设置为hidden。但是(总有一个“但是”),这种做法会导致在元素的位置上显示一片空白,所以我们需要将position属性设置为absolute,以便将元素移出正常的可视区

避免布局抖动:尽可能的减少浏览器执行大量的重新计算(重回或者重排),React的虚拟DOM,模拟实际DOM来实现极佳的性能。当我们在React中开发应用程序时,我们可以对虚拟DOM执行所有修改,而不考虑布局抖动。然后,在恰当的时候,React会使用虚拟DOM来判断对实际DOM需要做什么改变,以保证UI同步。这种创新的批处理方式,进一步提高了应用程序的性能。所以大部分时候都是异步更新的

事件循环

事件循环不仅仅包含事件队列,而是具有至少两个队列,除了事件,还要保持浏览器执行的其他操作。这些操作被称为任务,并且分为两类:宏任务(或通常称为任务)和微任务。

宏任务的例子很多,包括创建主文档对象、解析HTML、执行主线(或全局)JavaScript代码,更改当前URL以及各种事件,如页面加载、输入、网络事件和定时器事件。从浏览器的角度来看,宏任务代表一个个离散的、独立工作单元。运行完任务后,浏览器可以继续其他调度,如重新渲染页面的UI或执行垃圾回收。

而微任务是更小的任务。微任务更新应用程序的状态,但必须在浏览器任务继续执行其他任务之前执行,浏览器任务包括重新渲染页面的UI。

事件循环的实现至少应该含有一个用于宏任务的队列和至少一个用于微任务的队列。大部分的实现通常会更多用于不同类型的宏任务和微任务的队列。这使得事件循环能够根据任务类型进行优先处理。例如,优先考虑对性能敏感的任务,如用户输入。另一方面,由于在市面上的浏览器和JavaScript执行环境多如牛毛,所以如果发现所有任务都在一个队列的事件循环,也不要过分惊讶。

事件循环基于两个基本原则:● 一次处理一个任务。● 一个任务开始后直到运行完成,不会被其他任务中断。

处理宏任务和微任务队列之间的区别:单次循环迭代中,最多处理一个宏任务(其余的在队列中等待),而队列中的所有微任务都会被处理。

所有微任务会在下一次渲染之前执行完成,因为它们的目标是在渲染前更新应用程序状态

浏览器通常会尝试每秒渲染60次页面,以达到每秒60帧(60 fps)的速度。60fps通常是检验体验是否平滑流畅的标准,比方在动画里——这意味着浏览器会尝试在16ms内渲染一帧。需要注意图13.1所示的“更新渲染”是如何发生为何事件循环内的,因为在页面渲染时,任何任务都无法再进行修改。这些设计和原则都意味着,如果想要实现平滑流畅的应用,我们是没有太多时间浪费在处理单个事件循环任务的。理想情况下,单个任务和该任务附属的所有微任务,都应在16ms内完成

因为移动鼠标将导致大量的事件进入队列,因此在鼠标移动的处理函数中执行任何复杂操作都可能导致Web应用的糟糕体验。

setTimeout函数只到期一次,setInterval函数则不同,setInterval会持续执行直到被清除。因此,在第20ms时,setInterval又一次触发。但是,此时间隔计时器的实例已经在队列中等待执行,该触发被中止。浏览器不会同时创建两个相同的间隔计时器

计时器提供一种异步延迟执行代码片段的能力,至少要延迟指定的毫秒数。因为JavaScript单线程的本质,我们只能控制计时器何时被加入队列中,而无法控制何时执行

要记住的重要概念是,事件循环一次只能处理一个任务,我们永远不能确定定时器处理程序是否会执行我们期望的确切时间。间隔处理程序尤其如此。在这个例子中我们看到,尽管我们预定间隔在10、20、30、40、50、60和70ms时触发,回调函数却在34、42、50、60和70ms时执行。在本例中,少执行了两次回调函数,有几次回调函数没有在预期的时间点执行。

setTimeout内的代码在前一个回调函数执行完成之后,至少延迟10ms执行(取决于事件队列的状态,等待时间只会大于10ms);而setInterval会尝试每10ms执行回调函数,不关心前一个回调函数是否执行

在复杂应用开发中JavaScript单线程特性是最大的问题。当JavaScript忙于执行时,在浏览器上的用户交互会变得迟钝,甚至无响应。由于当JavaScript执行时,重新渲染页面的更新都被暂停,浏览器将会卡顿,看起来似乎处于假死状态。若脚本执行超过5s仍未停止,大多数浏览器会弹出警告对话框,提示用户脚本无响应,部分其他浏览器甚至会悄悄停止运行超过5s的脚本。=> 解决方案,把一个长时间运行的任务切割成很多很多细小的任务进行(或者使用一个计时器来中断一个长时间运行的任务

事件处理

在事件处理器内部,我们可以使用this关键字。通常来说在事件处理器内部,this指向事件发生的对象,但很快我们会发现,这并不准确。this关键字指向事件处理器所注册的元素(不一定是发生事件的元素

我们可以向addEventListener传递参数,很容易地选择希望的事件处理顺序。第3个参数如果传入true,将采用事件捕获;如果传入false,则采用事件冒泡。因此,某种意义上来说,W3C标准更倾向于优先选择事件冒泡,默认是事件冒泡。

在祖先元素上代理事件。利用的是事件冒泡原理(event.target.xxx=> 找到目标元素)

自定义事件如何在页面中使用

● 使用计时器,将计算开销很高的代码分解成可管理的、不阻塞浏览器的代码块。

● DOM是元素的分层树,发生在一个元素(target)上的事件通常是通过DOM进行代理的,有以下两种机制。

  • 事件捕获模式:事件从顶部元素向下传递到目标元素。

  • 事件冒泡模式:事件从目标元素向上冒泡到顶部元素。

● 当调用事件处理器时,浏览器也会传入一个事件对象。通过该对象的属性可访问发生事件的目标元素。通过处理器,使用this关键字引用在处理器上注册过的元素。

● 通过内置的CustomEvent构造函数和dispatchEvent方法,创建和分发自定义事件,减少应用程序不同部分之间的耦合。


在技术的历史长河中,虽然我们素未谋面,却已相识已久,很微妙也很知足。互联网让世界变得更小,你我之间更近。

在逝去的青葱岁月中,虽然我们未曾相遇,却共同经历着一样的情愫。谁的青春不曾迷茫或焦虑亦是无奈,谁不曾年少过

在未来的日子里,让我们共享好的文章,共同学习进步。有不错的文章记得分享给我,我不会写好的文章,所以我只能做一个搬运工

我叫 sunseekers(张敏) ,千千万万个张敏与你同在,18年电子商务专业毕业,毕业后在前端搬砖

如果喜欢我的话,恰巧我也喜欢你的话,让我们手拉手,肩并肩共同前行,相互学习,互相鼓励

文档信息

Search

    Table of Contents