最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 万字长文,深入JS核心语法(上篇)(es6类&继承的实现机制)

    正文概述 掘金(Hlianfa)   2021-03-17   638

    目录

    前言
      一、编译原理
        - JavaScript 如何运行
        - JIT
      二、执行上下文与作用域
        - 执行上下文
        - 作用域
        - 修改作用域
        - 暂时性死区
      三、原型与继承
        - 原型与原型链
        - JS中的继承
        - 性能
    后记
    

    前言

    文中无特殊标明的都是指在浏览器环境下的语法特性

    一、编译原理

    众所周知,我们写的语言,机器是“听不懂”的,需要借助翻译把我们的代码转化成机器能理解的语言(二进制文件)。

    我们写的语言统称为 编程语言。
    (不乏有个别大佬直接撸机器语言)

    根据“翻译”时间先后的不同还可细分为 编译型语言、解释型语言。

    1. 编译型语言 在代码运行之前,需要提前 “翻译”(编译) 好,并且编译之后会直接保留机器能读懂的二进制文件,以便之后每次运行时,都可以直接运行二进制文件,而不需要再次重新编译。常见的编译型语言有 C、C++、C#、Java 等。
    2. 解释型语言 也被称为 脚本语言。在每次运行时才去做“翻译”,有点 “同声传译” 内味了。常见的解释型语言有 Python、VBScript、ActionScript 等。

    在 编译型语言 与 机器码 间充当 “翻译” 角色的就是 编译器。
    编译 是一个复杂的过程,大致包括 词法分析、语法分析、语义分析、性能优化、生成可执行文件 五个步骤。这里不深究,《编译原理》学的不够扎实,不敢随意探讨。

    万字长文,深入JS核心语法(上篇)(es6类&继承的实现机制) 作者:@月舟

    解释型语言 与 机器码 的 "翻译" 通常被称作 解释器。
    代码在执行前,需确保环境中已经安装了 解释器。 大致需要 词法分析、语法分析、语义分析、 解释执行 四个步骤。

    万字长文,深入JS核心语法(上篇)(es6类&继承的实现机制) 作者:@月舟

    JavaScript 如何运行

    首先,JavaScript 常被归类为 解释型语言

    但是,实际上现代的浏览器中运行 JS 是有 编译器 参与的,不过它不生成可以到处执行的 二进制文件,并且 编译 通常发生在代码运行前的几微妙,或是代码运行中。
    需要注意的是,这个并不是 JavaScript 或 TC39 要求的,而是 Mozilla 和 Google 的开发人员为了提升浏览器性能而引入的。

    接下来看一下 js 中的 解释器 编译器 具体是如何工作的

    我们都知道 js 代码运行在 v8 引擎

    万字长文,深入JS核心语法(上篇)(es6类&继承的实现机制)

    1. 生成抽象语法树(AST)
      • 分词(tokenize),也称为 词法分析,将一行行的源码拆解成一个个 token(语法上不可能再分的、最小的单个字符或字符串)
      • 解析(parse),又称为语法分析,将上一步生成的 token 数据,根据语法规则转为 AST。如果源码存在语法错误,这一步就会终止,并抛出 语法错误
    2. 以 AST 为蓝本生成 字节码 Ignition 根据 AST 生成字节码。
    3. 执行 Ignition 除了生成字节码,还负责解释执行字节码。一段字节码第一次执行时,Ignition 会逐条解释执行。

    看到这里有的同学就会问了,“都执行完了,你说的编译器呢?”

    万字长文,深入JS核心语法(上篇)(es6类&继承的实现机制)

    各位看官少安毋躁,听我解释

    JIT

    容我扔张图先

    万字长文,深入JS核心语法(上篇)(es6类&继承的实现机制)

    TurboFan 就是你们要的 编译器 惹。

    Ignition 在解释执行字节码时。

    • 如果发现一段代码被重复执行多次,这段代码就是所谓的 热点代码(HotSpot)
    • 这时 TurboFan 就会介入,把这段字节码直接编译机器码,机器码是啥,就是一份可以直接执行的二进制文件呀
    • 于是当这段 热点代码 再次被执行时,就会直接执行编译后的机器码,不需要再通过 字节码 “翻译” 为 机器码,大大提升了代码的执行效率。

    这个技术呢,就叫做 即时编译(JIT)
    正是因为这个技术的存在,所以才有人说 V8 代码执行时间越久,执行效率越高

    一言蔽之就是:
    v8 引擎在 Ignition(点火)启动以后,代码段(机器部件)开始变热(warm),运行的久了就开始发烫(HotSpot),同时配合 TurboFan (涡轮增压)极大地提升了引擎效率。

    二、执行上下文与作用域

    执行上下文

    执行上下文(execution context),顾名思义,就是代码的执行环境。
    主要作用是 跟踪代码的执行情况。

    执行上下文大致可分为 3 类:

    1. 全局上下文:为运行代码主体而创建的执行上下文,为那些存在于 JavaScript 函数之外的任何代码而创建的。页面关闭后销毁。
    2. 函数上下文:每个函数会在执行的时候创建自己的执行上下文。这个上下文就是通常说的 本地上下文(local context)。函数执行完毕后销毁。
    3. eval 上下文:使用 eval() 函数创建的一个执行上下文。

    每个上下文创建的时候会被推入 执行上下文栈。当退出的时候,它会从上下文栈中移除。

    • 代码开始运行时,全局上下文 被创建。
    • 当需要执行函数时,在执行开始前 函数的执行上下文 被创建,并被推入 执行上下文栈 中。
    • 函数中调用另一个函数或代码块(es6)时,当前 可执行上下文 被挂起,一个新的执行上下文被创建,并压入栈中。
    • 当前代码块执行完毕后,弹出上下文栈,上一个被挂起的上下文继续执行;执行完毕后出栈。
    • 代码执行完毕,主程序退出,全局执行上下文从执行栈中弹出。此时栈中所有的上下文都已经弹出,程序执行完毕。

    (注:执行上下文栈 最顶部的可执行上下文被称为 running execution context)

    ES3、ES5、ES9 三个阶段中 执行上下文 所包含的内容是不同的

    ES3
    • variable object:变量对象,用于存储变量的对象。
    • scope:作用域,也常常被叫做作用域链。
    • this
    ES5
    • variable environment:变量环境, 当声明变量时使用。(此环境还包含了一个外部引用,用来指向外部的执行上下文,我们把这个外部引用称为 outer(外部环境))
    • lexical environment:词法环境, 当获取变量时使用。
    • this
    ES9
    • variable environment:变量环境,当声明变量时使用。
    • lexical environment:词法环境,当获取变量或者 this 值时使用。
    • code evaluation state:用于恢复代码执行位置。
    • Realm:使用的基础库和内置对象实例
    ES9 额外内容
    • Function:执行的任务是函数时使用,表示正在被执行的函数。
    • ScriptOrModule:执行的任务是脚本或者模块时使用,表示正在被执行的代码。
    • Generator:仅生成器上下文有这个属性,表示当前生成器

    作用域

    作用域 是个抽象的概念,指在执行上下文中变量与函数的可访问范围;起到隔离变量、函数的作用,使不同作用域的变量、函数相互独立。

    具体实现机制是 词法环境(lexical encironment),ES3 中使用 scope 实现,主要作用就是跟踪标识符和特定变量之间的映射关系。

    从上一小节中知道,词法环境(lexical encironment)是存储在 执行上下文中的,因此,作用域 也可以看作是 执行上下文 的组成部分。

    js 代码的执行需要经过 语法/词法分析、Ignition 解释执行/TurboFan 编译执行。在分析阶段 作用域 被确定,执行之前 执行上下文 被创建,并保存 作用域(词法环境) 信息。

    换言之,在代码编写时,作用域就已经确定;这种作用域也被叫做 词法作用域。
    (注:与之对立的是 动态作用域,在运行时才确定其作用域)

    作用域分类:

    • 全局作用域
    1. 最外层函数 和在最外层函数外面定义的变量拥有全局作用域
    2. 所有末定义直接赋值的变量 自动声明为拥有全局作用域
    3. 所有 window 对象的属性拥有全局作用域
    • 块级作用域(ES6 引入)
    1. 在一个函数内部
    2. 在一个代码块(由一对花括号包裹)内部
    • 函数作用域(ES6 前) 在函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部被访问。函数执行结束之后,函数内部定义的变量会被销毁。

    作用域链 即作用域的嵌套,当前 作用域 访问不到一个变量时,会沿着 作用域链 一层层向上查找。
    看个小栗子

    var name = "Bob";
    function foo() {
      console.log(name);
    }
    function bar() {
      var name = "Ben";
      foo();
    }
    bar(); // Bob
    

    简单分析一下这个例子

    • 调用 bar,bar 函数中声明了一个 name 变量,值为 “Ben”,同时调用 foo
    • foo 函数中输出 name 变量的值,foo 函数中找不到 name 变量,于是沿着 作用域链 向上查找
    • 由于 js 是 词法作用域,因此 foo 的上一层作用域是 全局作用域,而不是 bar 函数的作用域
    • 在全局作用域中找到 值为 “Bob” 的 name 变量,打印输出

    万字长文,深入JS核心语法(上篇)(es6类&继承的实现机制)

    修改作用域

    想要修改作用域是不太容易的,毕竟 js 是 词法作用域,书写时就已经确定了。
    但是,我们仍可以通过两个 evel 函数 和 with 来达到修改作用域的目的。

    eval

    function show(execute) {
      eval(execute);
      console.log(str);
    }
    
    var str = "hello world";
    var execute = 'var str = "hello javaScript"';
    
    show(execute); // hello javaScript
    

    在上面的例子中,如果不执行 eval 函数 的话,毫无疑问最后输出的会是 hello world,执行了 eval 函数后,实际的 show 函数变成了这样

    function show() {
      var str = "hello javaScript";
      console.log(str);
    }
    

    show 函数中多了 str 变量,所以最后打印输出的是 show 函数内部的 str 而不是全局的 str 变量。

    with

    with 语法可以帮我们更便利的读取对象中的属性(es6 的解构出现前),同时它也会创建一个作用域。

    function change(animal) {
      with (animal) {
        say = "moo";
      }
    }
    
    var dog = {
      say: "bark",
      size: "small",
    };
    var bull = {
      size: "big",
    };
    
    change(dog);
    change(bull);
    console.log(dog.say); // moo
    console.log(say); // moo
    

    这个例子在 严格模式 下运行会报错,简单分析一下这个例子

    • 执行 change(dog) 时,change 函数内创建了一个 with 作用域,其中的变量包括 say 和 size,with 中对 say 重新赋值,这里的传参属于 引用传递。因此 dog.say 从 “bark” 变成了 “moo”。
    • 执行 change(bull) 时,change 函数内同样创建了一个 with 作用域,其中的变量只有 size,但是,在 with 中,对一个不存的变量 say 赋值,在非严格模式下,没有声明的变量都会变成全局变量,因此,在全局作用域中多了一个 say 变量,且值为 “moo”。

    实际开发中,还是慎用 eval 和 with。

    暂时性死区

    提一嘴 暂时性死区,这一概念随着 es6 中的 let const 声明语句引入。 看下面这个例子

    function do_something() {
      console.log(bar); // undefined
      console.log(foo); // ReferenceError
      var bar = 1;
      let foo = 2;
    }
    

    我们都知道,var 声明的变量存在 变量提升,bar 的声明会被提升,等价于下面的代码

    function do_something() {
      var bar;
      console.log(bar); // undefined
      console.log(foo); // ReferenceError
      bar = 1;
      let foo = 2;
    }
    

    bar 变量已经声明,但未被赋值,所以输出 undefined。

    访问 foo 时直接报 引用错误,说明 let 不存在变量的提升,也可以说 foo 处在一个自块顶部到初始化处理的 暂时性死区 中。const 同理。

    三、原型与继承

    原型与原型链

    在 JS 中只有一种结构,那就是 对象,包括 function 也只是一个 Function 对象而已,同时,Object 对象是所有对象的“祖宗”。

    每个实例对象( object )又都有一个私有属性(称之为 __proto__ )指向它的构造函数的原型对象(prototype )。
    实例的构造函数的原型对象又会指向它的 构造函数 的原型对象,一层层往上,最后指向 Object 的构造函数的 prototype,而它的 __proto__ 最终会指向 null。
    一条完整的“链路”形成,即所谓的 原型链。

    以下是 MDN 关于 原型链 的定义:

    有点绕,通过一个例子理解一下

    const a = {
      name: "张三",
      gender: "男",
      say: function () {
        console.log(`I am ${this.name}`);
      },
    };
    const b = {
      name: "李四",
      gender: "女",
      say: function () {
        console.log(`I am ${this.name}`);
      },
    };
    

    ? 这个例子中,我们声明了两个对象,a、b 分别存了张三与李四的个人信息。 a、b 对象它们的 __proto__ 会指向它们的 构造函数 的原型对象,此时指向的就是 Object 构造函数的 prototype

    回到例子本身,这种方式看着不太优雅,如果我们要再加一个 王五,又要再写一遍 name、gender,有点繁琐。
    在其它编程语言中(如:C++、Java),通常做法会通过声明一个 类 来解决,但是,我们的 js 没有 类。
    (es6 的 class 只是语法糖,本质上还是构造函数)

    在 js 中通常使用 构造函数 来模拟类,并通过 new 运算符实例化。

    new 的作用:

    • 将实例的 __proto__ 属性指向 构造函数 的 prototype 属性
    • 将内部的 this 绑定到实例对象上。

    ok,现在再来改造一下上面的例子

    function Person(name, gender) {
      this.name = name;
      this.gender = gender;
    }
    
    // 不放入 Person 防止每次新建都被赋值一次
    Person.toString = function () {
      console.log("I am a person");
    };
    Person.prototype.say = function () {
      console.log(`Hi! I am ${this.name}`);
    };
    
    /* 也可使用 es6 的 class
      class Person {
        constructor(name, gender) {
          this.name = name;
          this.gender = gender;
        }
        static toString() {
          console.log('I am a person');
        }
        say() {
          console.log(`Hi! I am ${this.name}`);
        }
      }
    */
    const a = new Person("李四", "女");
    const b = new Person("张三", "男");
    
    a.say(); // Hi! I am 李四
    b.say(); // Hi! I am 张三
    

    张三与李四有个共同点,都是人嘛 ~
    于是我们搞了一个 Person 函数,通过 new 实例化 Person。

    • new 运算符后跟的就是 构造函数,也就是这里的 Person
    • a、b 都是 Person 的实例
    • 此时 a、b 中的私有属性 __proto__ 都指向了 Person 的 prototype 属性
    • 因为 Person 的 prototype 属性是个对象,所以它的 __proto__ 又指向 Object 构造函数的 prototype 属性

    当我们访问 say 方法时,会先在实例中找,显然实例对象中只有 name、gender。
    于是,会再去 实例的原型对象 寻找,找到了 say 方法,直接调用。
    当然,也可能在原型对象中也找不到,这时就会去找原型对象的原型对象。

    关系图如下:

    万字长文,深入JS核心语法(上篇)(es6类&继承的实现机制)

    小结

    再来总结一下

    • 层级:

      • 对象都有私有属性 __proto__(非标准,由浏览器实现)
      • 构造函数(constructor)包含两个与原型有关的私有属性 prototype 和 __proto__
      • __proto__(也可以说是 constructor 的 prototype) 属性下又包含两个私有属性 constructor 和 __proto__(Object 的 __proto__ 为 null)
    • 关系:

      • 对象的 __proto__ 指向 构造函数(constructor)的 prototype
      • prototype 下的 __proto__ 又会指向上一级的 构造函数 的 prototype,形成原型链
      • 顶层是 Object 构造函数的 prototype,它的 __proto__ 最终指向 null

    JS 中的继承

    现在 张三、李四 长大了,要开始工作了,李四成了一名教师,张三成了法外狂徒。

    ? 这里我们新建了 Teacher 和 OutLaw 两个类,并使用 寄生组合继承 的方式实现对 Person 的继承。
    (es6 extends 的简化版)

    // 继承
    function extend(child, parent) {
      // 子类构造函数的 prototype 的 proto 指向父类构造器的 prototype,继承父类的方法
      child.prototype = Object.create(parent.prototype);
      child.prototype.constructor = child;
      // 子类构造函数的 proto 指向父类构造器,继承父类的静态方法
      child.__proto__ = parent;
    }
    
    // 教师
    function Teacher(name, gender, lesson) {
      // 子类构造器里调用父类构造器,继承父类的属性
      Person.call(this, name, gender);
      this.lesson = lesson;
    }
    
    extend(Teacher, Person);
    
    // 重写 say 方法,属性遮蔽
    Teacher.prototype.say = function () {
      console.log(`Hi! I am a ${this.lesson} teacher`);
    };
    
    // 法外狂徒
    function OutLaw(name, gender) {
      Person.call(this, name, gender);
    }
    extend(OutLaw, Person);
    OutLaw.prototype.say = function () {
      console.log("阿巴阿巴阿巴...");
    };
    
    const a = new Teacher("李四", "女", "English");
    const b = new OutLaw("张三", "男");
    
    a.say(); // Hi! I am a English teacher
    b.say(); // 阿巴阿巴阿巴...
    

    结合下图与注释理解

    万字长文,深入JS核心语法(上篇)(es6类&继承的实现机制)

    ? es6 完整写法:

    class Person {
      constructor(name, gender) {
        this.name = name;
        this.gender = gender;
      }
      static toString() {
        console.log("I am a person");
      }
      say() {
        console.log(`Hi! I am ${this.name}`);
      }
    }
    
    class Teacher extends Person {
      constructor(name, gender, lesson) {
        super(name, gender);
        this.lesson = lesson;
      }
      say() {
        console.log(`Hi! I am a ${this.lesson} teacher`);
      }
    }
    
    class OutLaw extends Person {
      constructor(name, gender) {
        super(name, gender);
      }
      say() {
        console.log("阿巴阿巴阿巴...");
      }
    }
    
    const a = new Teacher("李四", "女", "English");
    const b = new OutLaw("张三", "男");
    
    a.say(); // Hi! I am a English teacher
    b.say(); // 阿巴阿巴阿巴...
    

    性能

    在遍历对象的属性时,原型链上的每个可枚举属性都会被枚举出来。如果只是要检查对象是否具有自己定义的属性,而不是其原型链上的某个属性,可以使用从 Object.prototype 继承的 hasOwnProperty 方法,避免找不到属性时,查找整个原型链。

    function Person(name, gender) {
      this.name = name;
      this.gender = gender;
    }
    const a = new Person('张三', '男');
    
    for(key in a) {
      if(a.hasOwnProperty(key)) {
        const ele = a[key];
        // do something
      }
    }
    

    后记

    如有其它意见,欢迎评论区讨论。文章同时发在个人公众号,欢迎关注 MelonField 深入JS核心语法

    参考:

    • www.yuque.com/suihangadam…
    • www.ruanyifeng.com/blog/2011/0…
    • www.jianshu.com/p/6dd0e22ff…
    • developer.mozilla.org/zh-CN/docs/…
    • developer.mozilla.org/zh-CN/docs/…
    • developer.mozilla.org/zh-CN/docs/…
    • developer.mozilla.org/zh-CN/docs/…

    起源地下载网 » 万字长文,深入JS核心语法(上篇)(es6类&继承的实现机制)

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元