深入理解JS|青训营笔记

91 阅读21分钟

课程内容

  1. JS的基本概念
  2. JS是怎么执行的
  3. JS的进阶知识点
  4. 课程总结

js基本概念

基本特点

  • 借鉴C语言的基本语法
  • 借鉴java的数据类型和内存管理
  • 借鉴SCHEME语言,将函数提升到一等公民身份
  • 借鉴SELF语言,使用基于原型(ProtoType)的继承机制

浏览器进程与渲染进程的展开

image.png

补充:进程与线程的区别

  • 进程是资源分配的最小单位;线程是cpu调度的最小单位
  • 一个进程包含多个线程;多个线程共享进程的方法区(元空间),但每个线程都有自己的程序计数器(流程控制、记录执行位置)、堆栈(保护局部变量独自访问)和局部变量
  • 进程有自己独立地址空间;线程没有自己独立的地址空间
  • 进程切换开销大,耗费资源大;线程开销小
  • 进程的并发性低;线程的并发性高
  • 进程可以独立执行;线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制
  • 系统运行时会为进程分配内存空间;线程 CPU 外,系统不会为线程分配内存,共享进程的资源
  • 一个进程崩溃后,其它进程不受影响(火车着火,其他火车不着火);一个线程崩溃,整个进程都死掉(火车着火,车厢全部会着火)

js基本概念

  1. 单线程: GUI线程和JS线程不能同时执行,互斥。

image.png 2. 动态,弱类型 定义变量不需要指定类型,而在其他语言中,比如C,C++都需要指定类型

image.png

  1. 面向对象编程,函数式编程

面向对象编程的思维方式是把现实世界中的事物抽象成程序世界中的类和对象,通过封装、继承和多态来演示事物事件的联系。

在JS中使用函数式编程,要求我们尽量使用纯函数,将大部分的逻辑的主要功能进行抽离,封装成新的函数。在调用该函数的时候只需传入需要的配置,就能实现向实例中添加新功能的作用。

// 将字符串转换为大写并添加感叹号
const toUpperCase = str => str.toUpperCase();
const exclaim = str => `${str}!`;
const shout = str => exclaim(toUpperCase(str));

console.log(shout('hello world')); // 输出: HELLO WORLD!

纯函数:

  • 相同输入产生相同输出(没有依赖除了输入的外部环境)
  • 无副作用(没有改变包括输入在内的外部环境)

数组的方法:

  • slice 截取纯函数:返回新数组,不会修改原数组,纯函数
  • splice 拼接非纯函数:返回被删的数组,会修改原数组,非纯函数

纯函数判断

// 纯函数
function sum(num1, num2) {
  return num1 + num2 
}

// 非纯函数(依赖 dep)
let dep = 5
function add(num){
  return dep + num
}
console.log(add(5)) // 10
dep = 10
console.log(add(5)) // 15

// 非纯函数(修改了输入)
let person = { name: 'nevermore', age: 23 }

function modify(obj) {
  obj.age = 100
}
modify(person)
console.log(person)
  1. 解释类语言 JIT

JS是一种解释型语言,程序在运行时边编译边运行,因此需要用到解释器才可执行代码,且执行效率天生比编译型语言低

JIT:即时编译,是一种编译技术,它可以在运行时将代码编译为机器语言,以提高程序的执行效率。

`主要流程

  1. Parser 解析器:JS 源代码经过解析器的词法分析(将 JS 代码拆分成一个个词法单元 token)和语法分析(将词法单元 token 根据语法规则组合成 AST 抽象语法树 ),分析过程中如果语法有错,会抛出语法错误。
  2. Ignition 解释器:AST 通过 Ignition 解释器转化为 btye code 字节码并执行。在执行过程中,如果发现重复执行多次的代码,则标记为热点代码,将热点代码交给 TurboFan 编译器处理。 (题外话:Java 字节码open in new window 是 Java 虚拟机执行的一种指令格式,V8 引擎本质是 JavaScript 的虚拟机)
  3. TurboFan 优化编译器:编译器拿到解释器标记的热点代码后,把它编译为更高效的机器码储存起来,等到下次再执行到这段代码时,就会用现在的机器码替换原来的字节码进行执行,这样大大提升了代码的执行效率。当一段代码不再是热点代码后,进行 deoptimization 去优化处理还原成字节码。`
  1. 安全,性能差

JavaScript是一种基于对象和事件驱动并具有相对安全性的客户端脚本语言。它不允许访问本地的硬盘,不能将数据存入服务器,不允许对网络文档进行修改和删除。边解释,边执行,所以性能会相对差,

数据类型

image.png

7 种值类型(原始类型):字符串(string)、数字(number)、布尔(boolean)、空(null)、未定义(undefined)、符号(symbol)、大数(bigInt

1 种引用类型:对象(object)

注意:[数组(array)、函数(function)属于对象]。

  • 值类型存放在栈中
  • 引用类型的地址值存在栈中,本身的值存在堆中。
作用域

规定了变量分为可访问性和可见性,作用域分为动态作用域和静态作用域,js是静态作用域,通过作用域可以知道代码如何查找标识符。js中的作用域分为三种,全局作用域,函数作用域,块作用域。

image.png

全局作用域是指在整个程序中都可以访问的变量和函数,它们被定义在程序的最外层。在全局作用域中定义的变量和函数可以被程序中的任何部分所访问。

局部作用域是指在函数内部定义的变量和函数。在函数内部定义的变量和函数只能在该函数内部被访问。这种作用域也被称为函数作用域。

块作用域是指在花括号{}内定义的变量和函数,只在该块内部可见,超出该块的范围后就无法访问。

JavaScript 中的作用域是基于词法作用域的,也被称为静态作用域。这意味着变量和函数的作用域在代码编写时就已经确定了,而不是在运行时根据调用的位置动态确定。

JavaScript 中的作用域链是一个由多个作用域对象组成的链。当需要访问一个变量时,JavaScript 引擎会首先在当前函数的作用域中查找该变量,如果找不到,则会沿着作用域链一级一级地向上查找,直到找到该变量或者到达全局作用域。如果在整个作用域链上都找不到该变量,则会抛出一个 ReferenceError 异常。

变量提升

变量提升(hoisting)是JavaScript中的一个特性,它指的是在执行代码之前,变量和函数声明会被移动到它们所在作用域的顶部。

举例

console.log(x); // 输出: undefined
var x = 10;

在这个例子中,我们在声明变量 x 之前就使用了它。由于变量提升的原因,这段代码实际上相当于:

var x;
console.log(x); // 输出: undefined
x = 10;

变量 x 被提升到了它所在作用域的顶部,并且被初始化为 undefined。因此,在声明变量 x 之前使用它不会报错,而是输出 undefined

函数声明也会被提升,下面是一个例子:

foo(); // 输出: Hello, world!

function foo() {
  console.log('Hello, world!');
}

在这个例子中,我们在声明函数 foo 之前就调用了它。由于函数声明提升的原因,这段代码实际上相当于:

function foo() {
  console.log('Hello, world!');
}

foo(); // 输出: Hello, world!

函数 foo 被提升到了它所在作用域的顶部,因此,在声明函数 foo 之前调用它不会报错,而是正常输出 Hello, world!

a()
var a = foo(); // 输出: Hello, world!

function foo() {
  console.log('Hello, world!');
}
// Uncaught TypeError: a is not a function

如果像上面那样将函数赋值给a,还是会报错,因为上面这段代码相当于

// 变量提升 
var a = undefined

a() // undefined不是一个function
var a = foo(); // 输出: Hello, world!

function foo() {
  console.log('Hello, world!');
}
// Uncaught TypeError: a is not a function

改为let,做一个实验

a()
let a = foo(); 

function foo() {
  console.log('Hello, world!');
}
// Cannot access 'a' before initialization

let a = foo();,这里使用了 let 关键字来声明变量 a。使用 let 和 const 声明的变量不会被提升,所以会在第一行 a() 会报错,因为在这一行之前并没有定义变量 a。在 JavaScript 中,如果你试图访问一个未定义的变量,就会抛出一个错误。

js是怎么执行的

1、AST为什么不能直接转为机器码呢?还要先转为字节码才可以转为机器码呢?

字节码的代码量比机器码少得多,可以节约内存的开销。

2、图中的最右侧部分其实就是我们说的JIT,JIT具有热编译机制,当JVM发现某个方法或代码块运行特别频繁的时候,就会认为这是“热点代码”(Hot Spot Code)。JIT会把部分“热点代码”编译成本地机器相关的机器码,并进行优化,然后再把编译后的机器码缓存起来,以备下次使用。

image.png

image.png

执行上下文

词法环境中存储了当前执行上下文中所有的本地变量、函数声明、形参等信息,以及对外部词法环境的引用。

image.png

全局代码执行过程

初始化全局对象

js引擎会在执行代码之前,会在堆内存创建一个全局对象:Global Object(GO)

  • 该对象所有的作用域都可以访问;
  • 里面包含DateArrayStringNumbersetTimeoutsetInterval等;
  • 其中还有一个window属性指向自己;

执行上下文栈

  1. v8引擎为了执行代码, v8引擎内部会有一个执行上下文栈(Execution Context Stack, ECStack)(函数调用栈)
  2. 因为我们执行的是全局代码, 为了全局代码能够正常的执行, 需要创建 全局执行上下文(Global Execution Context)(全局代码需要被执行时才会创建)

GEC被放入到ECS中里面包含两部分内容:

  • 第一部分:在代码执行前,在parser转成AST的过程中,会将全局定义的变量、函数等加入到GlobalObject中,但是并不会赋值;这个过程也称之为变量的作用域提升

  • 第二部分:在代码执行中,对变量赋值,或者执行其他的函数

GEC被放入到ECS中:

全局代码执行过程1 GEC开始执行代码:

全局代码执行过程2

函数代码执行过程

在执行的过程中执行到一个函数时,就会根据函数体创建一个函数执行上下文(Functional Execution Context,简称FEC),并且压入到ECStack中。

FEC中包含三部分内容:

  • 第一部分:在解析函数成为AST树结构时,会创建一个Activation Object(AO)(AO中包含形参、arguments、函数定义和指向函数对象、定义的变量)
  • 第二部分:作用域链:由VO(在函数中就是AO对象)和父级VO组成,查找时会一层层查找
  • 第三部分:this绑定的值

FEC被放入到ECS中:

函数代码执行过程1 FEC开始执行代码:

函数代码执行过程2 函数代码执行过程3

注意:

  • 当我们查找一个变量时,真实的查找路径是沿着作用域链来查找
  • 函数的父级作用于跟它定义的位置有关,与调用位置没有关系

具体例子详细说明

var scop = 'global'; // 声明了一个全局变量 `scop` 并将其赋值为字符串 `'global'`
func() // 调用了名为 `func` 的函数。由于这个函数在后面定义,所以它可以在这里调用

function func() { // 义一个名为 `func` 的函数。
  var funcVar = 'func'; // 函数内部声明了一个局部变量 `funcVar` 并将其赋值为字符串 `'func'`。
  console.log('这是一个函数', funcVar); 
  // 调用了 `console.log` 函数,
  // 输出字符串 `'这是一个函数'` 和变量 `funcVar` 的值。
}
var company = '字节' // 声明了一个全局变量 `company` 并将其赋值为字符串 `'字节'`

image.png

创建执行上下文时做了什么?

image.png

词法环境、变量环境、outer
  • 词法环境: 存放let const 定义的变量以及函数。
  • 变量环境:存放var 定义的变量。
  • outer:指向外部函数的一个指针。

image.png

堆和栈

基本类型:采用的是值传递

引用类型:则是地址传递

引用类型的数据的地址指针是存储于栈中的,将存放在栈内存中的地址赋值给接收的变量。当我们想要访问引用类型的值的时候,需要先从栈中获得对象的地址指针,然后,在通过地址指针找到堆中的所需要的数据(保存在堆内存中,包含引用类型的变量实际上保存的不是变量本身,而是指向该对象的指针)。

  • 引用赋值(又称引用传址):某个变量或常量存放了指向引用类型(对象、数组、函数)字面量的地址。
  • 传值赋值:某个变量或常量存放了基本类型的字面量
  • 浅拷贝:在堆中创建新的内存保存拷贝后的对象,拷贝前后对象的基本类型数据互不影响;但拷贝前后对象的引用类型数据因为指向同一个内存地址,相互影响。
  • 深拷贝:在堆中创建新的内存保存拷贝后的对象,拷贝前后对象的基本类型、引用类型数据互不影响。
原对象字面量是否指向同一地址原对象字面量第一层为基本类型是否相互影响原对象字面量包含子对象是否相互影响
引用赋值
浅拷贝
深拷贝

image.png

ESP指针

image.png

js进阶知识点

闭包

内部函数引用外部函数的变量或参数。本质是一个没有被回收的对象,会产生内存泄漏。 举例:

var n = 10
function fn(){
    var n =20
    function f() {
       n++;
       console.log(n)
     }
    return f
}

var x = fn()
x() // 21

image.png

为了避免内存泄漏,可以在不再需要闭包时将其设置为 null。这样,闭包所引用的外部变量就不再被引用,它们就可以被垃圾回收器回收。

定义

不严谨:闭包是能够访问外层函数作用域中的自由变量的函数

广义(理论):闭包是能够访问外层作用域中的自由变量的函数与这个自由变量组成的词法环境

狭义(实践):闭包是有访问外层函数作用域中的自由变量的函数与这个自由变量组成的词法环境

闭包最大的作用是可以在内层函数中访问到其外层函数的作用域。

每个函数在预编译阶段都会生成一个空的闭包对象,无论这个闭包是否被使用。当函数执行完毕,函数实例被销毁,如果函数内部引用了外部自由变量,将自由变量加入到闭包对象中,闭包会被内层函数的作用域链引用,不会被回收;否则空的闭包没有被引用,会被释放回收。

广义

从理论(广义)角度,所有函数在创建时都会创建闭包,无论这个闭包是否被使用。函数执行完毕,没有被使用到的闭包会被回收。

var a = 1
function foo() {
  console.log(a)
}
foo()

foo 引用了外层全局作用域的变量 a,创建了闭包,但因为全局作用域是长久存在的,所以该闭包多此一举,函数执行完毕也就被回收了。

狭义

从实践(狭义)角度,我们只关注:因为内层函数引用外层函数作用域的自由变量,依然存在、不被回收的闭包,即使创建这个闭包的外层函数(作用域)都已销毁。

function foo() {
  let n = 'foo n'
  function bar() {
    console.log(n)
    debugger // 闭包 (foo){ n: "foo n" }
  }
  return bar
}
let tmp = foo()
tmp() // foo n

内层函数 bar 引用了外层函数作用域的变量 a,外层函数 foo 创建了闭包,但因为函数作用域是随着函数执行完毕就被销毁的,为了内层函数能够引用外层函数的变量,该闭包是必须存在、不能被回收的。

this

this 指向函数的调用者,其中有 5 种绑定规则:

this 指向

在 JavaScript 中,this 关键字的指向取决于函数的调用方式。下面是一些常见的情况:

  1. 在全局作用域中,this 指向全局对象。在浏览器中,全局对象是 window 对象。
console.log(this === window); // 输出 true
  1. 在函数中,this 的指向取决于函数的调用方式。如果函数作为一个对象的方法被调用,那么 this 指向这个对象。
let obj = {
  value: 'I am an object property',
  method: function() {
    console.log(this.value);
  }
};

obj.method(); // 输出 'I am an object property'
  1. 如果函数不是作为对象的方法被调用,那么 this 指向全局对象。
function func() {
  console.log(this === window);
}

func(); // 输出 true

4、new 构造函数中的this 关键字指向新创建的对象

使用new关键字来调用函数是,会执行如下的操作:

  1. 在内存中创建一个空的临时对象
  2. 将这个临时对象的隐式原型 [[Prototype]] 指向构造函数显式原型 prototype
  3. 绑定 this 到这个临时对象上
  4. 执行构造函数内部的代码(给新对象添加属性)
  5. 返回这个临时对象

function Car(make, model, year) {
  this.make = make;
  this.model = model;
  this.year = year;
}

let myCar = new Car('zhangsan', 'bc', 2022);

console.log(myCar.make); // 输出 'zhangsan'
console.log(myCar.model); // 输出 'bc'
console.log(myCar.year); // 输出 2022

this补充知识点

在严格模式下,如果函数不是作为对象的方法被调用,那么 this 的值为 undefined

'use strict';

function func() {
  console.log(this);
}

func(); // 输出 undefined

可以使用 callapply 或 bind 方法显式地设置 this 的值。

function func() {
  console.log(this.value);
}

let obj1 = { value: 'I am object 1' };
let obj2 = { value: 'I am object 2' };

func.call(obj1); // 输出 'I am object 1'
func.apply(obj2); // 输出 'I am object 2'

let boundFunc = func.bind(obj1);
boundFunc(); // 输出 'I am object 1'

规则优先级

1.显式绑定高于隐式绑定

function foo() {
  console.log(this)
}

let obj = {
  name: 'obj',
  foo: foo.bind('aa')
}

obj.foo() // String {'aa'}

2.new 绑定高于隐式绑定

let obj = {
  foo: function () {
    console.log(this)
  }
}

let foo1 = new obj.foo() // foo {}

3.new 绑定高于 bind 绑定

new 不能与 apply/call 一起使用,只能与 bind 同时使用

// new 的优先级高于 bind
function foo() {
  console.log(this)
}

let bar = foo.bind('aa')

let obj = new bar() // foo {}

4.bind 高于 call

有点反常理,理应后面覆盖前面。

bind 后就不能再更改绑定了。

function foo() {
  console.log(this)
}
foo.bind('aa').call('bb') // String {'aa'}

// foo.call('aa').bind('bb') 报错:call 绑定后执行返回 undefined,无法 bind

详情请看Nevermore毓的学习笔记-this指向

image.png

垃圾回收

v8对GC(回收垃圾的)的优化

分代式垃圾回收:v8 的垃圾回收策略主要基于分代式垃圾回收机制,V8 中将堆内存分为新生代和老生代两区域。

新生代垃圾回收:用了一种复制式的方法即 Cheney算法 ,Cheney算法 将堆内存一分为二,一个是处于使用状态的空间我们暂且称之为 使用区,一个是处于闲置状态的空间我们称之为 空闲区

新加入的对象都会存放到使用区,当使用区快被写满时,就需要执行一次垃圾清理操作,使用区和空闲区互相交换,

生命周期较长的对象或者空闲区空间占用超过了 25%  ,那么这个对象会被直接晋升到老生代空间中,原因是当完成 垃圾 回收后,空闲区将翻转成使用区,继续进行对象内存的分配,若占比过大,将会影响后续内存分配。

新生代回收使用并行回收提高效率并行回收依然会阻塞主线程

老生代垃圾回收

整个流程就采用的就是上文所说的标记整理算法

采用增量标记、惰性清理,并发回收提高效率

增量标记是指将一次垃圾回收的过程分为很多个小部分,每次执行完一小块,就让应用程序执行一会,交替执行。增量标记采用三色标记法(暂停与恢复)与写屏障(增量中修改引用)

惰性清理指的是让js脚本先执行,也无需一次性清理完所有非对象内存。

并发回收:主线程在执行 JavaScript 的过程中,辅助线程能够在后台完成执行垃圾回收的操作,辅助线程在执行垃圾回收的时候,主线程也可以自由执行而不会被挂起

image.png

事件循环

JS 是单线程的、非阻塞的。通过事件循环解决了单线程会阻塞的问题。JS 实现异步的核心就是事件循环

早期的 JS 作为浏览器脚本语言,为了防止 DOM 渲染冲突的问题、简化编程,被设计为单线程语言。

如今,为了充分发挥 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JS 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM,所以 Web Worker 本质上仍是通过单线程模拟多线程。

总结:优点是简化编程,缺点是无法发挥 CPU 的全部性能(可以使用 HTML5 新标准 Web Worker实现多线程)

js代码主要分为两大类: 同步代码、异步代码

  1. 异步代码又分为:微任务与宏任务

  2. 事件循环Event Loop执行机制

    • 1.进入到script标签,就进入到了第一次事件循环.

    • 2.遇到同步代码,立即执行

    • 3.遇到宏任务,放入到宏任务队列里.

    • 4.遇到微任务,放入到微任务队列里.

    • 5.执行完所有同步代码

    • 6.执行微任务代码

    • 7.DOM 渲染(若有则渲染,无则跳过)(微任务会阻塞页面的渲染,宏任务不会)

    • 8.微任务代码执行完毕,本次队列清空

    • 9.执行宏任务:在此之前,先查看微任务队列是否为空,不为空则继续执行微任务(先微后宏)即:若微任务在执行过程中产生了新的微任务,则继续执行微任务,微任务执行完毕后,再回到宏任务中进行下一轮微任务队列

    以此反复直到清空所以宏任务,这种不断重复的执行机制,就叫做事件循环

    宏任务主要包含:main script(主线程代码)、setTimeoutsetIntervalsetImmediaterequestAnimationFrameI/O事件、DOM 监听事件、AJAX 请求、UI 页面渲染

    微任务主要包含:promise thenasync/awaitMutationObserver(H5 新特性)、queueMicrotaskprocess.nextTick

执行过程:在所有同步任务执行完毕之后,异步任务会优先执行所有已经存在任务队列中的微任务。在所有的微任务执行完毕之后,再去宏任务队列中执行一个(注意是一个)宏任务,执行完一个宏任务之后会再去微任务队列中检查是否有新的微任务,有则全部执行,再回到宏任务队列执行一个宏任务,以此循环。 执行顺序

练习题

例题一

setTimeout(() => { // 宏1
  console.log('3')
  Promise.resolve().then(() => {
    console.log('4') // 产生了新的微任务,放入第二轮微任务
  })
})
console.log('1')
Promise.resolve().then(() => { // 第一轮 微任务
  console.log('2') // 先微后宏
  setTimeout(() => {
    console.log('5') // 宏2,在执行宏任务前,先查看微任务队列是否为空,不为空则继续执行微任务
  })
})

例题二

async function async1() {
  console.log('async1 start') // 2
  await async2() // 同步执行 async2
  console.log('async1 end') // await 的下边代码进入微任务队列 (微1) 6
}

async function async2() {
  console.log('async2') // 3
}

console.log('script start') // 1

setTimeout(function () { //(宏1) 8
  console.log('setTimeout')
}, 0)

async1()

new Promise(function (resolve) {
  // 初始化 Promise,传入的函数同步执行
  console.log('promise1') // 4
  resolve()
}).then(function () { // (微2) 7 (微任务执行完毕,接着执行宏任务)
  console.log('promise2')
})

console.log('script end') // 5 (同步代码执行完毕,接着执行微任务)

await 与 Promise 的等价替换:

async function async1() {
  console.log('1')
  await async2()
  console.log('3')
}
async function async2() {
  console.log('2')
}
// 等价于
function async1() {
  console.log('1')
  Promise.resolve(async2()).then(() => {
    // 执行 async2 的返回值放入 resolve 里
    console.log('3')
  })
}
// 紧跟着 await 后面的语句相当于放在了 new Promise 中
// 下一行及之后的语句相当于放在于 Promise.then 中

例题三

// 整个 setTimeout 的回调函数放入宏任务队列(宏1),微任务结束再执行
setTimeout(() => {
  console.log('setTimeout1') // 7
  new Promise((resolve) => {
    resolve()
  }).then(() => {
    // 宏1 引入了新的微任务,放入第二轮的微任务队列中,先把微任务队列执行完后,再执行宏2
    new Promise(function (resolve) {
      resolve()
    }).then(() => {
      // 放入第三轮微任务队列中
      console.log('then4') // 9
    })
    console.log('then3') // 8 同步执行
  })
})

new Promise((resolve) => {
  console.log('promise1') // 1
  resolve()
}).then(() => {
  console.log('then1') // (微1)4
})

setTimeout(() => {
  console.log('setTimeout2') // 10
}) // (宏2)

console.log(2) // 2

queueMicrotask(() => {
  console.log('queueMicrotask') // (微2)5
})

new Promise((resolve) => {
  console.log('promise2') // 3
  resolve()
}).then(() => {
  console.log('then2') // (微3)6 (第一轮微任务执行完毕,执行宏1)
})

// promise1
// 2
// promise2
// then1
// queueMicrotask
// then2
// setTimeout1
// then3
// then4
// setTimeout2

图解:

例题四

console.log('start');
    setTimeout(() => {
      console.log('children2');
      Promise.resolve().then(() => {
        console.log('children3');
      })
    }, 0);

    new Promise(function (resolve, reject) {
      console.log('children4');
      setTimeout(function () {
        console.log('children5');
        resolve('children6')
      }, 0)
    }).then((res) => {
      console.log('children7');
      setTimeout(() => {
        console.log(res);
      }, 0)
    })
    
 // start children4 children2 children3 children5 children7 children6