重学JavaScript(1)

112 阅读8分钟

Atwood定律

Stack Overflow的创立者之一的 Jeff Atwood 在2007年提出了著名的 Atwood定律: Any application that can be written in JavaScript, will eventually be written in JavaScript. 任何可以使用JavaScript来实现的应用都最终都会使用JavaScript实现。

浏览器内核

事实上,我们经常说的浏览器内核指的是浏览器的排版引擎:排版引擎(layout engine),也称为浏览器引擎(browser engine)、页面渲染引擎(rendering engine) 或样版引擎。

  • Gecko:早期被Netscape和Mozilla Firefox浏览器使用;

  • Trident:微软开发,被IE4~IE11浏览器使用,但是Edge浏览器已经转向Blink;

  • Webkit:苹果基于KHTML开发、开源的,用于Safari,Google Chrome之前也在使用;

  • Blink:是Webkit的一个分支,Google开发,目前应用于Google Chrome、Edge、Opera等;

渲染引擎工作的过程

  • 执行过程中,HTML解析的时候遇到了JavaScript标签,应该怎么办呢?
    • 会停止解析HTML,而去加载和执行JavaScript代码
  • 为什么不直接异步去加载执行JavaScript代码,而要在这里停止掉呢?
    • 这是因为JavaScript代码可以操作我们的DOM, 所以浏览器希望将HTML解析的DOM和JavaScript操作之后的DOM放到一起来生成最终的DOM树,而不是 频繁的去生成新的DOM树;
  • JavaScript代码由JavaScript引擎来执行

JavaScript引擎

  1. JavaScript引擎的作用

我们编写的JavaScript无论你交给浏览器或者Node执行,最后都是需要被CPU执行的; 但是CPU只认识自己的指令集(机器语言),才能被CPU所执行; 所以我们需要JavaScript引擎帮助我们将JavaScript代码翻译成CPU指令来执行;

  1. 比较常见的JavaScript引擎

    • SpiderMonkey:第一款JavaScript引擎,由Brendan Eich开发(也就是JavaScript作者);
    • Chakra:微软开发,用于IT浏览器;
    • JavaScriptCore:WebKit中的JavaScript引擎,Apple公司开发;
    • V8:Google开发的强大JavaScript引擎,也帮助Chrome从众多浏览器中脱颖而出

WebKit内核

  • WebKit的组成:
    • WebCore:负责HTML解析、布局、渲染等等相关的工作;
    • JavaScriptCore:解析、执行JavaScript代码;
  • 微信小程序:WebVew和JsCore

V8引擎

  • V8引擎的定义:

  • V8是用C ++编写的Google开源高性能JavaScript和WebAssembly引擎,它用于Chrome和Node.js等。

  • 它实现ECMAScript和WebAssembly,并在Windows 7或更高版本,macOS 10.12+和使用x64,IA-32, ARM或MIPS处理器的Linux系统上运行。

  • V8可以独立运行,也可以嵌入到任何C ++应用程序中。

  • Parse模块会将JavaScript代码转换成AST(抽象语法树),这是因为解释器并不直接认识JavaScript代码;

  • 如果函数没有被调用,那么是不会被转换成AST的;

  • Parse的V8官方文档:v8.dev/blog/scanne…

  • Ignition是一个解释器,会将AST转换成ByteCode(字节码)

  • 同时会收集TurboFan优化所需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算);

  • 如果函数只调用一次,Ignition会执行解释执行ByteCode;

  • Ignition的V8官方文档:v8.dev/blog/igniti…

  • TurboFan是一个编译器,可以将字节码编译为CPU可以直接执行的机器码;

    • 如果一个函数被多次调用,那么就会被标记为热点函数,那么就会经过TurboFan转换成优化的机器码,提高代码的执行性能;
    • 但是,机器码实际上也会被还原为ByteCode,这是因为如果后续执行函数的过程中,类型发生了变化(比如sum函数原来执行的是number类型,后 来执行变成了string类型),之前优化的机器码并不能正确的处理运算,就会逆向的转换成字节码;
    • TurboFan的V8官方文档:v8.dev/blog/turbof…
  • 上面是JavaScript代码的执行过程,V8的内存回收也是其强大的另外一个原因:

    • Orinoco模块,负责垃圾回收,将程序中不需要的内存回收;
    • Orinoco的V8官方文档:v8.dev/blog/trash-…

单线程

  • js是单线程语言,只能同时做一件事;

  • 浏览器和Nodejs已经支持js启动线程,如Web Worker

  • js和DOM渲染共同一个线程,因为js可以修改DOM结构

基本类型和引用类型

  • 基本类型
  1. undefined:类型表示不存在定义,声明变量但没有初始化,这个变量的值就是undefined( 注意:在任何一个引用变量值设置为undefined都是错误的);
  2. null:表示一个值被定义了,定义为空值; 使用场景为 定义变量准备在将来用于保存对象;所以引用值可以是null而不会是undefined;undefined和null的区别:js诞生的时候只设置了null作为“无”的值。最初的设计是null是表示一个“无”的对象,转为数值时为0; undefined表示“无”的原始值,转为数值时为NaN;红宝书上说引入undefined就是为了正式的区分空对象指针与未经初始化的变量,变量设置为null就是空对象指针,没有设置就是未经初始化;
  3. number:字面量格式可以是十进制、八进制(八进制第一位必须是0)、十六进制(前两位必须是0x);
  4. string:由零个或多个16位Unicode字符组成的字符序列;
  5. boolean:字面值为true和false;
  6. symbol:ES5 的对象属性名都是字符串,这容易造成属性名的冲突。比如,你使用了一个他人提供的对象,但又想为这个对象添加新的方法(mixin 模式),新方法的名字就有可能与现有方法产生冲突。如果有一种机制,保证每个属性的名字都是独一无二的就好了,这样就从根本上防止属性名的冲突。这就是ES6 引入Symbol的原因 ;
  • 引用类型:

    • Object 类型、Array 类型、Date 类型、RegExp 类型、Function 类型 等;
    • null是特殊的引用类型,指向克隆地址,其余5个基本类型是值类型;
  • 添加属性时候的区别

    • 引用类型可以动态的给添加属性,但是基本类型的值是不可变也不可以复制的
  • 变量的不同内存分配

    • 原始值:存储在栈(stack)中的简单数据段,也就是说,它们的值直接存储在变量访问的位置。这是因为这些原始类型占据的空间是固定的,所以可将他们存储在较小的内存区域 – 栈中。这样存储便于迅速查寻变量的值;
    • 引用值:存储在堆(heap)中的对象,也就是说,存储在变量处的值是一个指针(point),指向存储对象的内存地址。这是因为:引用值的大小会改变,所以不能把它放在栈中,否则会降低变量查寻的速度。相反,放在变量的栈空间中的值是该对象存储在堆中的地址。地址的大小是固定的,所以把它存储在栈中对变量性能无任何负面影响;
  • 两种类型在复制值的什么有什么区别?

    • 基本类型复制值的时候,会重新在变量对象上创建一个新值,然后把值赋值到新变量分配的空间上来(理解为栈中的空间),即值传递;
    • 引用类型复制时,也会在储存在变量对象中的值复制一份放到为新变量分配的空间上来,但是不同的是新复制的值是一个指针,指向原值所在堆内存中的地址(复制内存地址),即引用传递;
    • 对于引用类型的变量,==和===只会判断引用的地址是否相同,而不会判断对象具体里属性以及值是否相同。因此,如果两个变量指向相同的对象,则返回true。

闭包

  • 自由变量的查找,是在函数定义的地方向上级作用域查找,而不是在执行的地方

    function print1(fn) {
        const a1 = 200
        fn()
    }
    const a1 = 100
    function fn1() {
        console.log(a1)//100
    }
    print1(fn1)
    function print2(fn) {
        const a2 = 200
        return function () {
            console.log(a2)//200
        }
    }
    const a2 = 100
    const fn2 = print2()
    fn2()
    

this

  • 谁调用就指向谁;

  • 箭头函数会将this 指向当前定义时的this (取上级作用域的this );

改变this指向

  • call():使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数;

  • apply():与call类似,唯一区别是:接受的是一个包含多个参数的数组;

  • bind():创建一个新的函数,在 bind()被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

class 类 和 原型链

class 类

  • 继承类声明extends

  • 注意,构造函数中使用的 super() 只能在构造函数中使用,并且必须在使用 this 关键字前调用;

class Person {
    constructor(name) {
        this.eyes = 2
        this.name = name
    }
    eat() {
        console.log('eat something')
    }
}
class Student extends Person {
    constructor(name, number) {
        super(name)
        this.number = number
    }
    code() {
        console.log('hello world')
    }
}
let jayhan = new Student('jayhan', 123)
console.log(jayhan.name)
console.log(jayhan.number)
jayhan.eat()
jayhan.code()

原型链

// 原型链
jayhan.__proto__ === Student.prototype
Student.prototype.__proto__ === Person.prototype
Person.prototype.__proto__  === Object.prototype
Object.prototype.__proto__ === null

隐式调用

  • 每个对象的toString和valueOf方法都可以被改写,每个对象执行完毕,如果被用以操作JavaScript解析器就会自动调用对象的toString或者valueOf方法
let a = {
    i: 10,
    toString: function () {
        console.log('tostring')
        return this.i
    },
    valueOf: function () {
        console.log('valueOf')
        return this.i
    }
}
console.log(a == 10) // 'valueOf' true 运算符,字符串拼接
console.log(String(a)) // 'tostring' '10' ,操作对象时