循序渐进学 JavaScript <三>

续<二>

十五、深入浏览器的渲染原理

15.1 网页的解析过程

  • 输入域名URL -> 经过DNS(域名)解析 -> ·转成对应的IP 地址 -> 获取资源(从服务器(主机电脑 => 有自己的IP地址)中获取) -> 返回资源 -> 浏览器进行解析
  • 只下载 html ,浏览器会根据 link , script 元素请求另外一个资源

15.2 浏览器的内核

  • 在浏览器中把 html 、css 、js进行解析的是浏览器内核
  • 常见的浏览器内核有
    • Trident(三叉戟):IE、360安全浏览器、搜狗高速浏览器、百度浏览器、UC浏览器
    • Gecko(壁虎):Mozilla Firefox
    • Presto(急板乐曲)->Blink(眨眼):Opera
    • Webkit:Safari、360极速浏览器、搜狗高速浏览器、移动端浏览器(Android、iOS)
    • Webkit->Blink:Google Chrome,Edge
  • 浏览器内核指的是浏览器的排版引擎:
    • 排版引擎(layout engine),也称为浏览器引擎(browser engine)、页面渲染引擎(rendering engine)或样版引擎
  • dom树 + css 规则 —> 渲染树 (layout) —> paint 绘制 —> display
    • 渲染树上有节点,但是没有位置信息

15.3 解析一:HTML 解析过程

  • 解析 HTML,形成 dom tree

15.4 解析二:生成 css 规则

  • 在解析的过程中,如果遇到CSS的link元素,那么会由浏览器负责下载对应的CSS文件
  • 注意:下载 **CSS **文件是不会影响 DOM 的解析的,可能会影响渲染树
  • 浏览器下载完 CSS 文件后,就会对 CSS 文件进行解析,解析出对应的规则树
  • 可以称之为 CSSOM (CSS Object Model,CSS对象模型)

15.5 解析三:构建 Render Tree

  • 有了DOM Tree和 CSSOM Tree后,就可以两个结合来构建 Render Tree了
  • 注意一:link 元素不会阻塞DOM Tree的构建过程,但是会阻塞 Render Tree的构建过程
  • 这是因为Render Tree在构建时,需要对应的CSSOM Tree;
  • 注意二:Render Tree 和 DOM Tree 并不是一一对应的关系,比如对于 display为none的元素,压根不会出现在 render tree 中

15.6 解析四:布局(layout)和绘制(Paint)

  • 第四步是在渲染树(Render Tree)上运行布局(Layout)以计算每个节点的几何体(大小位置信息)
    • 渲染树会表示显示哪些节点以及其他样式,但是不表示每个节点的尺寸、位置等信息
    • 布局是确定呈现树中所有节点的宽度、高度和位置信息;
  • 第五步是将每个节点绘制(Paint)到屏幕上
    • 在绘制阶段,浏览器将布局阶段计算的每个 frame 转为屏幕上实际的像素点
    • 包括将元素的可见部分进行绘制,比如文本、颜色、边框、阴影、替换元素(比如img)

循序渐进学 JavaScript <三>_第1张图片

15.7 回流和重绘

  • 回流 reflow (重排、回流)
    • **布局 **: 第一次确定节点的大小和位置
    • 回流 : 之后对节点的大小/位置重新计算
    • 只要你有重新做布局,重新计算大小
  • 引起回流: => 回流一定会引起重绘
    • DOM 结构改变
    • 改变了布局(width,height,padding,font-size)
    • 窗口 resize 尺寸
    • 调用 getComputedStyle 获取尺寸、位置
  • 重绘 repaint:
    • 绘制:第一次渲染内容
    • 重绘:之后重新渲染,再次做一次绘制,性能影响比较小
  • 引起重绘
    • 修改背景色、文字颜色、边框颜色、样式
  • 避免发生回流
    • 修改样式时尽量一次性修改: cssText 、添加 class 修改
    • 尽量避免频繁的操作 DOM
      • 在一个documentFragment(片段) 或者父元素中将要操作的DOM操作完成
      • 再一次性地操作
    • 尽量避免通过 getComputedStyle 获取尺寸/位置
    • 对某些元素使用 position 中的 absolute 或者 fixed :
      • 开销相对较小
      • 脱标元素不影响其他元素

15.8 特殊解析五- composite 合成

  • 绘制的过程,可以将局部后的元素绘制到多个合成图层

  • 标准流里面的元素 -> layout tree -> render layer1

  • absolute->render layer2

  • -----> 多个图层合并,合成(合成图层)12

  • 默认情况下,标准流中的内容都是被绘制在同一个图层中

    • 虽然是不同的渲染层
  • 而一些特殊的属性,会创建一个新的合成层(CompositingLayer),并且新的图层可以利用 GPU(处理图形的渲染) 来加速绘制,提高页面性能

    • 可以形成新的合成层的属性
      • 3D transforms
      • video、canvas、iframe
      • opacity 动画转换时
      • position: fixed
      • will-change:一个实验性的属性,提前告诉浏览器元素可能发生哪些变化
      • animation 或 transition 设置了 opacity、transform
    • 因为每个合成层都是单独渲染的
  • 分层确实可以提高性能,但是它以内存管理为代价,因此不应作为 web 性能优化策略的一部分过度使用

15.9 script 元素和页面解析的关系

  • 遇到 link 元素还是会下载对应的 css 文件,但还是会继续 html 的解析

  • 但是,浏览器在解析 HTML 的过程中,遇到了 script 元素是不能继续构建 DOM 树的

  • 它会停止继续构建,首先下载 JavaScript 代码,并且执行 JavaScript 的脚本

  • 只有等到 JavaScript 脚本执行结束后,才会继续解析 HTML ,构建 DOM 树

  • 为什么要这样做呢?

    • 这是因为 JavaScript 的作用之一就是操作 DOM ,并且可以修改 DOM
    • 如果等到 DOM 树构建完成并且渲染再执行 JavaScript,会造成严重的回流和重绘,影响页面的性能
    • 所以在遇到 script 元素时,优先下载和执行 JavaScript 代码,再继续构建 DOM 树
  • 这个也往往会带来新的问题,特别是现代页面开发中

    • 在目前的开发模式中(比如 Vue、React ),脚本往往比 HTML 页面更“重”,处理时间(下载->执行)需要更长;
    • 所以会造成页面的解析阻塞,在脚本下载、执行完成之前,用户在界面上什么都看不到
    • 为了解决这个问题,script 元素提供了两个属性(attribute):defer 和 async

15.10 defer 属性

  • defer 属性告诉浏览器不要等待脚本下载,而继续解析 HTML,构建 DOM Tree
  • 脚本会由浏览器来进行下载,但是不会阻塞 DOM Tree的构建过程
  • 如果脚本提前下载好了,它会等待 DOM Tree 构建完成,先执行 defer 中的代码
  • 再触发 DOMContentLoaded (DOM Tree 发出)事件
  • 两个都有 defer,保证第二个 demo 可以使用第一个脚本的 message ,所以从上到下执行
  • 多个带 defer 的脚本是可以保持正确的顺序执行的
  • 从某种角度来说,defer 可以提高页面的性能,并且推荐放到 head 元素中
  • 注意:defer 仅适用于外部脚本,对于 script 默认内容会被忽略
  • 需要操作 dom 使用 defer
DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Documenttitle>
  <script src="./js/test.js" defer>script>
  <script src="./js/demo.js" defer>script>
head>
<body>
  
  <div id="app">appdiv>
  <div class="box">div>
  <div id="title">titlediv>
  <div id="nav">navdiv>
  <div id="product">productdiv>

  
  
  <script>
    // 总结三: defer 代码是在 DOMContentLoaded事件发出之前执行
    window.addEventListener("DOMContentLoaded", () => {
      console.log("DOMContentLoaded")
    })
  script>
  
  <h1>哈哈哈哈啊h1>

body>
html>

15.11 async 属性

  • async 特性与 defer 有些类似,能够让脚本不阻塞页面。
  • async 是让一个脚本完全独立的
    • 浏览器不会因 async 脚本而阻塞(与 defer 类似);
    • 下载完了立马执行
    • async 脚本不能保证顺序,它是独立下载、独立运行,不会等待其他脚本
    • async 不会能保证在 DOMContentLoaded 之前或者之后执行
  • defer 通常用于需要在文档解析后操作 DOM 的 JavaScript 代码,并且对多个 script 文件有顺序要求
  • async 通常用于独立的脚本,对其他脚本,甚至 DOM 没有依赖的

十六、JavaScript 内存管理和闭包

16.1 内存管理

  • 不管什么样的编程语言,在代码的执行过程中都是需要给它分配内存的,不同的是某些编程语言需要自己手动的管理内存某些编程语言可以自动管理内存
  • 不管以什么样的方式来管理内存,内存的管理都会有如下的生命周期(历程)
    • 分配申请你需要的内存(申请)
    • 使用分配的内存(存放一些东西,比如对象等)
    • 不需要使用时,对其进行释放
  • 不同的编程语言对于第一步和第三步会有不同的实现
    • 手动管理内存:比如 C、C++,包括早期的 OC ,都是需要手动来管理内存的申请和释放的(malloc 和 free 函数)
    • 自动管理内存:比如 Java、JavaScript、Python、Swift、Dart 等,自动管理内存
  • 对于开发者来说,JavaScript 的内存管理是自动的、无形的。
    • 创建的原始值、对象、函数……这一切都会占用内存
    • 并不需要手动进行管理,JavaScript 引擎会帮助处理好它

16.2 JavaScript 的内存管理

  • JavaScript 会在定义数据时分配内存
  • 不同的内存分配方式
    • JS 对于原始数据类型内存的分配会在执行时,直接在栈空间进行分配
    • JS 对于复杂数据类型内存的分配会在堆内存中开辟一块空间,并且将这块空间的指针返回值变量引用

16.3 JavaScript 的垃圾回收

  • 垃圾

    • 程序不需要再使用的对象
    • 程序不能再访问到的对象
  • 因为内存的大小是有限的,所以当内存不再需要的时候,需要对其进行释放,以便腾出更多的内存空间

  • 手动管理内存的语言中,需要通过一些方式自己来释放不再需要的内存,比如 free 函数

    • 但是这种管理的方式其实非常的低效,影响编写逻辑的代码的效率
    • 并且这种方式对开发者的要求也很高,并且一不小心就会产生内存泄露
  • 所以大部分现代的编程语言都是有自己的垃圾回收机制

    • 垃圾回收的英文是 Garbage Collection,简称 GC
    • 对于不再使用的对象,都称之为是垃圾,它需要被回收,以释放更多的内存空间
    • 语言运行环境,比如Java的运行环境 JVM,JavaScript 的运行环境 js 引擎都会内存 垃圾回收器
    • 垃圾回收器也会简称为GC,所以在很多地方你看到GC其实指的是垃圾回收器
  • 但是这里又出现了另外一个很关键的问题:GC怎么知道哪些对象是不再使用的呢?

    • GC 的实现以及对应的算法

16.4 常见的 GC 算法

16.4.1 引用计数(Reference counting)

  • 引用计数
    • 当一个对象有一个引用指向它时,那么这个对象的引用就+1
    • 当一个对象的引用为0时,这个对象就可以被销毁掉
  • 这个算法有一个很大的弊端就是会产生循环引用
obj1 = {}
obj2 = {}
obj1.info = obj2
obj2.info = obj1

16.4.2 标记清除(mark-Sweep)

  • 标记清除:
    • 标记清除的核心思路是可达性(Reachability)
    • 这个算法是设置一个根对象(root object),垃圾回收器会定期从这个根开始,找所有从根开始有引用到的对象,对于哪些没有引用到的对象,就认为是不可用的对象
    • 这个算法可以很好的解决循环引用的问题
  • 遛狗法则
  • 缺点:产生空间碎片化的问题

16.4.3 其他算法优化补充

  • JS 引擎比较广泛的采用的就是可达性中的标记清除算法,当然类似于V8引擎为了进行更好的优化,它在算法的实现细节上也会结合一些其他的算法
  • 标记整理(Mark-Compact) 和“标记-清除”相似
    • 不同的是,回收期间同时会将保留的存储对象搬运汇集到连续的内存空间,从而整合空闲空间,避免内存碎片化
  • 分代收集(Generational collection)—— 对象被分成两组:“新的”和“旧的”
    • 许多对象出现,完成它们的工作并很快死去,它们可以很快被清理
    • 那些长期存活的对象会变得“老旧”,而且被检查的频次也会减少
    • V8 垃圾回收策略
  • 增量收集(Incremental collection)
    • 如果有许多对象,并且试图一次遍历并标记整个对象集,则可能需要一些时间,并在执行过程中带来明显的延迟
    • 所以引擎试图将垃圾收集工作分成几部分来做,然后将这几部分会逐一进行处理,这样会有许多微小的延迟而不是一个大的延迟
  • 闲时收集(Idle-time collection)
    • 垃圾收集器只会在 CPU 空闲时尝试运行,以减少可能对代码执行的影响

16.5 JavaScript的函数式编程 FP

  • JavaScript是支持函数式编程的

    • 函数式编程中的函数指的不是程序中的函数(方法),而是数学中的函数即映射关系,例如:y = sin(x),x和y的关系
    • 对运算过程的抽象,屏蔽实现的细节,只关注
    • 相同的输入要有相同的输出
  • 在JavaScript中,函数是非常重要的,并且是一等公民

    • 那么就意味着函数的使用是非常灵活的
    • 高阶函数:可以作为另外一个函数的参数,也可以作为另外一个函数的返回值来使用
  • 所以JavaScript存在很多的高阶函数

    • 自己编写高阶函数
    • 使用内置的高阶函数
  • 目前在 vue3+react

  • 开发中,也都在趋向于函数式编程

    • vue3 composition api: setup函数 -> 代码(函数hook,定义函数)
    • react:class -> function -> hooks
// 非函数式:面向过程
let num1 = 2
let num2 = 3
let sum = num1 + num2
console.log(sum)
// 函数式
function add (n1, n2) {
return n1 + n2
}
let sum = add(2, 3)
console.log(sum)

16.6 闭包

  • 之所以存在这么多对象,作用域和作用域链,都是为了闭包。闭包是通过作用域链实现的。

  • 计算机科学中 和在 JavaScript 中

  • 在计算机科学中对闭包的定义(维基百科)

    • 闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures)
    • 是在支持 头等函数 的编程语言中,实现词法绑定的一种技术
    • 闭包在实现上是一个结构体,它存储了一个函数和一个关联的环境(相当于一个符号查找表)
    • 闭包跟函数最大的区别在于,当捕捉闭包的时候,它的 自由变量 会在捕捉时被确定,这样即使脱离了捕捉时的上下文,它也能照常运行
  • 闭包的概念出现于60年代,最早实现闭包的程序是 Scheme,那么就可以理解为什么 JavaScript 中有闭包

    • 因为 JavaScript 中有大量的设计是来源于 Scheme 的
  • MDN 对 JavaScript 闭包的解释

    • 一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包closure
    • 闭包可以在一个内层函数中访问到其外层函数的作用域
    • 在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来
  • 总结

    • 一个普通的函数 function,如果它可以访问外层作用域的自由变量,那么这个函数和周围环境就是一个闭包
    • 从广义的角度来说:JavaScript 中的函数都是闭包
    • 从狭义的角度来说:JavaScript 中一个函数,如果访问了外层作用域的变量,那么它是一个闭包
  • 应用

// 求员工工资:基本工资 + 绩效
function getSalary(base){
    return function(performance){
        return base + performance
    }
}
let level1 = getSalary(10000)
const allSalart = level1(1200)

16.7 内存泄漏以及释放

  • 内存释放:
    • 对象:adder=null
    • 数组:adder=[]/null
  • 对于那些永远不会再使用的对象,但是对于 GC 来说,它不知道要进行释放的对应内存会依然保留着
  • 什么情况会出现内存泄露
    • 意外的全局变量 ==》开启严格模式
      • 没有声明直接赋值,放在 window 上面
      • 通过 this 创建意外的全局变量
    • 闭包
      • 闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多
    • dom 泄露
      • 为了减少DOM访问次数,一般情况下,当需要多次访问同一个DOM方法或属性时,会将DOM引用缓存到一个局部变量中。但如果在执行某些删除、更新操作后,可能会忘记释放掉代码中对应的DOM引用,这样会造成DOM内存泄露

16.8 浏览器的优化操作

  • AO 对象不会被销毁时,并不是里面的所有属性都不会被释放
 function foo() {
      var name = "foo"
      var age = 18
      var height = 1.88
      function bar() {
        debugger
        console.log(name)
      }
      return bar
    }
    var fn = foo()
    fn()

十七、JavaScript 函数的增强知识

17.1 函数对象的属性

  • 默认函数对象中已经有自己的属性

    • name 属性: console.log(foo.name)
      // 将两个函数放到数组中(了解)
        var fns = [foo, bar]
        for (var fn of fns) {
          console.log(fn.name)
        }
    
    • length 属性:返回函数参数的个数
      • 不会将剩余属性放在 length 里面
     function demo(...args) {
     }
     demo("abc", "cba", "nba")
    

17.2 arguments

  • arguments 是一个对应于传递给函数的参数的类数组(array-like)对象

  • 默认用法

     //通过索引获取内容
          console.log(arguments[0])
          console.log(arguments[1])
    
          // 遍历(可迭代对象)
          for (var i = 0; i < arguments.length; i++) {
            console.log(arguments[i])
          }
          for (var arg of arguments) {
            console.log(arg)
          }
    
  • 是一个对象类型

    • 但是它却拥有数组的一些特性,比如说 length,比如可以通过 index 索引来访问
    • 但是它却没有数组的一些方法,比如 filter、map、foreach 等
     // 数组 filter
          for (var arg of arguments) {
            if (arg % 2 === 0) {
              console.log(arg)
            }
          }
          var evenNums = arguments.filter(item => item % 2 === 0)//错误!
          console.log(eventNums)
    
  • 转成数组

      // 2.1.将arguments转成数组方式一:
          // var newArguments = []
          // for (var arg of arguments) {
          //   newArguments.push(arg)
          // }
          // console.log(newArguments)
    
          // 2.2.将arguments转成数组方式三: ES6中方式
    		// Array.from
          // var newArgs1 = Array.from(arguments)
          // console.log(newArgs1)
    		//展开运算符
          // var newArgs2 = [...arguments]
          // console.log(newArgs2)
    
          // 2.3.将arguments转成数组方式二: 调用slice(截取)方法
    //slice在内部对函数做了一个截取
    //显式绑定this
    //使用[]获取slice方法
          var newArgs = [].slice.apply(arguments)
          // var newArgs = Array.prototype.slice.apply(arguments)
          console.log(newArgs)
    

17.3 箭头函数不绑定arguments

  • 不是说不能用,是会在上层作用域进行查找
    • 全局作用域没有就去 window 找
   // 1.箭头函数不绑定arguments
    // var bar = () => {
    //   console.log(arguments)
    // }

    // bar(11, 22, 33)


    // 2.函数的嵌套箭头函数
    function foo() {
      var bar = () => {
        console.log(arguments)
      }
      bar()
    }

    foo(111, 222)

17.4 函数的剩余( rest )参数

  • ES6 中引用了 rest parameter,可以将不定数量的参数放入到一个数组中

  • …otherNums 最后一个参数是… 为前缀的,那么它会将剩余的参数放到该参数中,是一个数组

  • 剩余参数需要写到最后

  • 以前的时候为了拿到其它的参数,会使用 arguments

    • 不是数组,类数组
    • 还得剥除前面
    • 箭头函数没有 arguments
  • 用来替代 arguments,直接使用 rest

    // 剩余参数: rest parameters
    function foo(num1, num2, ...otherNums) {
      // otherNums数组
      console.log(otherNums)
    }

    foo(20, 30, 111, 222, 333)


    // 默认一个函数只有剩余参数
    function bar(...args) {
      console.log(args) // ["abc",123,"cba",321]
    }

    bar("abc", 123, "cba", 321)

    // 注意事项: 剩余参数需要写到其他的参数最后
  • 区别
    • 剩余参数只包含那些没有对应形参的实参,而 arguments 对象包含了传给函数的所有实参
    • arguments 对象不是一个真正的数组,而 rest 参数是一个真正的数组,可以进行数组的所有操作
    • arguments 是早期的 ECMAScript 中为了方便去获取所有的参数提供的一个数据结构,而 rest 参数是 ES6 中提供并且希望以此替代 arguments 的

17.5 JavaScript 纯函数

  • Pure

  • 函数式编程中有一个非常重要的概念叫纯函数,JavaScript 符合函数式编程的范式,所以也有纯函数的概念

  • 纯函数的维基百科定义

    • 在程序设计中,若一个函数符合以下条件,那么这个函数被称为纯函数
    • 此函数在相同的输入值时,需产生相同的输出
      • 有闭包就不一定
    • 函数的输出和输入值以外的其他隐藏信息或状态无关,也和由 I/O 设备产生的外部输出无关
    • 该函数不能有语义上可观察的函数副作用,诸如“触发事件”,使输出设备输出,或更改输出值以外物件的内容等
  • 总结:确定的输入一定产生确定的输出;函数在执行过程中,不能产生副作用

  • 函数副作用

    • 依赖于外部变量

      • 配置文件
      • 数据库
      • 获取用户的输入
    • 在执行一个函数时,除了返回函数值之外,还对调用函数产生了附加的影响,比如修改了全局变量,修改参数或者改变外部的存储

        // 不是一个纯函数
        var address = "广州市"
        function printInfo(info) {
          console.log(info.name, info.age, info.message)
          info.flag = "已经打印结束"
          address = info.address
        }
    
        var obj = {
          name: "lili",
          age: 18,
          message: "哈哈哈哈"
        }
    
        printInfo(obj)
    
        console.log(obj)
        if (obj.flag) {
          
        }
    
  • 举例

    • slice:slice 截取数组时不会对原数组进行任何操作,而是生成一个新的数组
    • splice:splice 截取数组, 会返回一个新的数组, 也会对原数组进行修改–不是纯函数
  • 作用和优势

    • 安心的写:不需要去关心外层作用域中的值目前是什么状态
    • 安心的用:调用函数时,确定的输入一定产生确定的输出
  • React 中就要求无论是函数还是 class 声明一个组件,这个组件都必须像纯函数一样,保护它们的 props 不被修改

  • 纯函数的好处

    • 可缓存,提高性能
    • 可以测试
    • 方便并行处理
      • js 本来是单线程的,但是 es6 之后开启了一个Web Worker,可以开启新的线程
      • 在多线程环境下并行操作共享的内存数据很可能会出现意外情况
      • 纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数

17.6 柯里化(一种转换)

  • 柯里化也是属于函数式编程里面一个非常重要的概念

    • 是一种关于函数的高阶技术
    • 它不仅被用于 JavaScript,还被用于其他编程语言
  • 维基百科的解释

    • 在计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化
    • 是把接收多个参数的函数,变成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数,而且返回结果的新函数的技术
        // 因为foo不是一个柯里化的函数, 所以目前是不能这样调用
        // 柯里化函数
        function foo2(x) {
          return function(y) {
            return function(z) {
              console.log(x + y + z)
            }
          }
        }
    
        foo2(10)(20)(30)
    
    • 柯里化声称“如果你固定某些参数,你将得到接受余下参数的一个函数”
  • 总结:只传递给函数一部分参数来调用它,让它返回一个函数去处理剩余的参数

  • 柯里化是一种函数的转换,将一个函数从可调用的 f(a, b, c) 转换为可调用的 f(a)(b)©

    • 柯里化不会调用函数,只是对函数进行转换
  • 优势

    • 职责单一
    • 参数复用

17.7 自动柯里化函数

  • 经过 liliCurrying(fn) 传入一个函数对象
  • 返回一个柯里化函数
  • 两类操作
    • 继续返回一个新的函数
      • 继续接受参数
    • 直接执行 fn 函数
   function liliCurrying(fn) {
      function curryFn(...args) {
        // 两类操作:
        // 第一类操作: 继续返回一个新的函数, 继续接受参数
        // 第二类操作: 直接执行fn的函数
        if (args.length >= fn.length) { // 执行第二类
          // return fn(...args)
          return fn.apply(this, args)
        } else { // 执行第一类
          return function (...newArgs) {
            // return curryFn(...args.concat(newArgs))
            return curryFn.apply(this, args.concat(newArgs))
          }
        }
      }

      return curryFn
    }

17.8 组合函数

  • 组合(Compose)函数是在 JavaScript 开发过程中一种对函数的使用技巧、模式
    • 比如现在需要对某一个数据进行函数的调用,执行两个函数 fn1 和 fn2,这两个函数是依次执行的
    • 那么如果每次都需要进行两个函数的调用,操作上就会显得重复
    • 可以将这两个函数组合起来,自动依次调用
    • 这个过程就是对函数的组合,称之为组合函数(Compose Function)
    // 第一步对数字*2
    function double(num) {
      return num * 2
    }

    // 第二步对数字**2
    function pow(num) {
      return num ** 2
    }

    console.log(pow(double(num)))
    console.log(pow(double(55)))
    console.log(pow(double(22)))

    // 将上面的两个函数组合在一起, 生成一个新的函数
    function composeFn(num) {
      return pow(double(num))
    }

  • 封装组合函数
 // 封装的函数: 你传入多个函数, 我自动的将多个函数组合在一起挨个调用
    function composeFn(...fns) {
      // 1.边界判断(edge case)
      var length = fns.length
      if (length <= 0) return
      for (var i = 0; i < length; i++) {
        var fn = fns[i]
        if (typeof fn !== "function") {
          throw new Error(`index position ${i} must be function`)
        }
      }

      // 2.返回的新函数
      return function(...args) {
        var result = fns[0].apply(this, args)
        for (var i = 1; i < length; i++) {
          var fn = fns[i]
          result = fn.apply(this, [result])
        }
        return result
      }
    }

17.9 with 语句的使用

  • 作用:扩展了作用域链
  • with(obj):明确指定了作用域
  • 不建议使用,混淆原有的作用域
    var obj = {
      message: "Hello World"
    }

    with (obj) {
      console.log(message)
    }

17.10 eval 函数

  • 内建函数 eval 允许执行一个代码字符串

    • eval 是一个特殊的函数,它可以将传入的字符串当做 JavaScript 代码来运行
    • eval 会将最后一句执行语句的结果,作为返回值
  • 不建议在开发中使用 eval

    • 可读性非常差
    • 字符串被篡改的危险
    • 必须经过 js 解释器,不能被 js 引擎优化

17.11 严格模式

  • JavaScript 历史的局限性

    • 长久以来,JavaScript 不断向前发展且并未带来任何兼容性问题
    • 新的特性被加入,旧的功能也没有改变,这么做有利于兼容旧代码
    • 但缺点是 JavaScript 创造者的任何错误或不完善的决定也将永远被保留在 JavaScript 语言
  • 在 ECMAScript5 标准中,JavaScript 提出了严格模式的概念(Strict Mode)

    • 严格模式很好理解,是一种具有限制性的 JavaScript 模式,从而使代码隐式的脱离了”懒散(sloppy)模式“
    • 支持严格模式的浏览器在检测到代码中有严格模式时,会以更加严格的方式对代码进行检测和执行
  • 严格模式对正常的 JavaScript 语义进行了一些限制

    • 严格模式通过抛出错误来消除一些原有的静默(silent)错误
    • 严格模式让 JS 引擎在执行代码时可以进行更多的优化(不需要对一些特殊的语法进行处理)
    • 严格模式禁用了在 ECMAScript 未来版本中可能会定义的一些语法

17.12 开启严格模式

  • 严格模式通过在文件或者函数开头使用“use strict” 来开启
  <script>
    // 给整个script开启严格模式
    "use strict"
	// 粒度化的控制
    // 给一个函数开启严格模式
    function foo() {
      "use strict"
    }
//默认就是在严格模式
    class Person {
      
    }


  </script>
  • 没有类似于 “no use strict” 这样的指令可以使程序返回默认模式
  • 现代 JavaScript 支持“ class ” 和“ module ”,它们会自动启用 use strict

17.13 严格模式限制

  • 无法意外地创建全局变量

  • 严格模式会使引起静默失败(silently fail,注:不报错也没有任何效果)的赋值操作抛出异常)

  • 严格模式下试图删除不可删除的属性

  • 严格模式不允许函数参数有相同的名称

  • 不允许 0 开头的八进制语法(0o)

  • 在严格模式下,不允许使用 with

  • 在严格模式下,eval 不再为上层创建变量

  • 严格模式下,this 绑定不会默认转化为对象

  • 独立函数执行默认情况下,绑定 window 对象

  • 在严格模式下,不绑定全局对象而是 undefined

    "use strict"
    // 1.不会意外创建全局变量
    // function foo() {
    //   message = "Hello World"
    // }

    // foo()
    // console.log(message)

    // 2.发现静默错误
    var obj = {
      name: "lili"
    }

    Object.defineProperty(obj, "name", {
      writable: false,
      configurable: false
    })

    // obj.name = "kobe"
    console.log(obj.name)

    // delete obj.name
    console.log(obj)

    // 3.参数名称不能相同
    // function foo(num, num) {

    // }

    // 4.不能以0开头
    // console.log(0o123)

    // 5.eval函数不能为上层创建变量
    // eval(`var message = "Hello World"`)
    // console.log(message)

    // 6.严格模式下, this是不会转成对象类型的
    function foo() {
      console.log(this)
    }
    foo.apply("abc")
    foo.apply(123)
    foo.apply(undefined)
    foo.apply(null)
    
    // 独立函数执行默认模式下, 绑定window对象
    // 在严格模式下, 不绑定全局对象而是undefined
    foo()

十八、JavaScript 对象的增强知识

18.1 对象属性的控制

  • 默认情况下属性都是没有特别的限制

  • 可以对对象中的属性进行某些限制,使用的不多

    • 但是 getter 和 setter 在 vue2 中做响应式操作
  • 对一个属性进行比较精准的控制操作,使用属性描述符

    • 精准的添加或修改对象的属性(精准的描述)
      • 这个属性不能被删除
      • 这个不能写入只能读取
      • 获取值时,只能获取xxx
      • 在遍历的时候不要出现
    • 属性描述符需要使用 Object.defineProperty 来对属性进行添加或者修改

18.2 Object.defineProperty

  • Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象

    • Object.defineProperty(obj,prop,descriptor)
    • 修改哪一个对象的哪一个属性进行哪些描述
  • 三个参数

    • obj:要定义属性的对象
    • prop:要定义或修改的属性的名称或Symbol
    • descriptor:要定义或修改的属性描述符
  • 返回值:返回被修改后的对象

18.3 属性描述符

  • 类型
    • 数据属性(Data Properties)描述符(Descriptor)
    • 存取属性(Accessor访问器 Properties)描述符(Descriptor)
      • 获取或存取的时候调用一些函数

18.4 数据属性描述符

  • Configurable
    • 配置能够通过 delete 删除属性,是否可以修改他的特性,或者是否可以将它修改为存取属性描述符
    • 直接在对象上定义属性时候,默认 Configurable 是 true
    • 通过通过属性描述符定义一个属性时 Configurable 默认添加为 false
  • Enumerable:表示属性是否可以通过枚举 for-in 或者 Object.keys() 返回该属性
  • Writable:表示是否可以修改属性的值
  • value:返回自己写入的 value
    var obj = {
      name: "lili", // configurable: true
      age: 18
    }

    Object.defineProperty(obj, "name", {
      configurable: false, // 告诉js引擎, obj对象的name属性不可以被删除
      enumerable: false, // 告诉js引擎, obj对象的name属性不可枚举(for in/Object.keys)
      writable: false, // 告诉js引擎, obj对象的name属性不写入(只读属性 readonly)
      value: "lili" // 告诉js引擎, 返回这个value
    })

    delete obj.name
    console.log(obj)

    // 通过Object.defineProperty添加一个新的属性
    Object.defineProperty(obj, "address", {})
    delete obj.address
    console.log(obj)

    console.log(Object.keys(obj))

    obj.name = "kobe"
    console.log(obj.name)

18.5 存取属性描述符

  • Configurable 和 Enumerable 方法

  • get:获取属性时会执行的函数,默认为 undefined,会监听读取的过程

  • set:function(value){log(“哈哈”,value)}

    • set:设置属性时会执行的函数,默认为 undefined,会监听写入的过程
    // vue2响应式原理
    var obj = {
      name: "lili"
    }

    // 对obj对象中的name添加描述符(存取属性描述符)
    var _name = ""//_有特殊含义,别乱用我啊
    Object.defineProperty(obj, "name", {
      configurable: true,
      enumerable: false,
      set: function(value) {
        console.log("set方法被调用了", value)
        _name = value//把值保存在_name里面
      },
      get: function() {
        console.log("get方法被调用了")
        return _name
      }
    })

    obj.name = "kobe"
    obj.name = "jame"
    obj.name = "curry"
    obj.name = "lili"

    // 获取值
    console.log(obj.name)

18.6 定义多个属性

  • Object.defineProperties() 方法直接在一个对象上定义 多个 新的属性或修改现有属性,并且返回该对象
    var obj = {
      name: "lili",
      age: 18,
      height: 1.88
    }

    // Object.defineProperty(obj, "name", {})
    // Object.defineProperty(obj, "age", {})
    // Object.defineProperty(obj, "height", {})

    // 新增的方法
    Object.defineProperties(obj, {
      name: {
        configurable: true,
        enumerable: true,
        writable: false
      },
      age: {
      },
      height: {
      }
    })

18.7 对象方法补充

  • 获取对象的属性描述符

    • getOwnPropertyDescriptor
    • getOwnPropertyDescriptors
  • 禁止对象扩展新属性:preventExtensions

    • 给一个对象添加新的属性会失败(在严格模式下会报错)
  • 密封对象,不允许配置和删除属性:seal

    • 实际是调用 preventExtensions
    • 并且将现有属性的 configurable:false
  • 冻结对象,不允许修改现有属性: freeze

    • 实际上是调用 seal
    • 并且将现有属性的 writable: false
    var obj = {
      name: "LIli",
      age: 18
    }

    // 1.获取属性描述符
    console.log(Object.getOwnPropertyDescriptor(obj, "name"))
    console.log(Object.getOwnPropertyDescriptors(obj))

    // 2.阻止对象的扩展
    Object.preventExtensions(obj)
    obj.address = "广州市"
    console.log(obj)

    // 3.密封对象(不能进行配置)
    Object.seal(obj)
    delete obj.name
    console.log(obj)

    // 4.冻结对象(不能进行写入)
    Object.freeze(obj)
    obj.name = "kobe"
    console.log(obj)

你可能感兴趣的:(javascript,开发语言,ecmascript,js,浏览器原理,前端,闭包)