• JavaScript进阶(Learning Records)


    背景:对JavaScript的深入学习
    参考:《JavaScript高级程序设计》《冴羽 JavaScript 深入》

    从原型到原型链

    prototype

    prototype是每个函数都会有的属性

    function Person(){
    }
     Person.prototype.name = 'Kevin';
    var person1 = new Person();
    var person2 = new Person();
    console.log(person1.name) // Kevin
    console.log(person2.name) // Kevin
    

    一个函数的prototype指向一个对象,这个对象是构造函数所创建的实例原型
    原型是什么:每一个JavaScript对象创建时都会关联另一个对象(除NULL),这个对象就是原型,其他对象从原型继承属性
    也是上述例子中person1和person2的原型
    img

    proto

    该属性是每个JavaScript对象所具有的属性,会指向该对象的原型
    承接上文

    console.log(person.__proto__ === Person.prototype) // true;
    

    img
    同样也有一个construct函数指向原构造函数

    console.log(Person === Person.prototype.construct) // true;
    

    img

    实例和原型的关系

    当我们想去读取实例的属性时,如果找不到实例的属性,就去找与实例关联的原型的属性,如果还找不到,就找原型的原型,就这样不断向上递归,找到最顶层为止

    function Person() {
     
    }
     
    Person.prototype.name = 'Kevin';
     
    var person = new Person();
     
    person.name = 'Daisy';
    console.log(person.name) // Daisy
     
    delete person.name;
    console.log(person.name) // Kevin
    
    

    实例和原型的具体关系如下:
    img
    其中蓝色的线就是原型链

    词法作用域和动态作用域

    作用域决定了当前代码对变量的访问权限
    词法作用域即静态作用域,函数的作用域在函数创建时决定。
    动态作用域,函数的作用域在函数调用的时候决定。

    var value = 1;
     
    function foo() {
     console.log(value);
    }
     
    function bar() {
     var value = 2;
     foo();
    }
    
    bar();
    

    由于JavaScript采用的是静态作用域,所以在foo中查找value时会到函数的上层去找,输出是1
    如果是动态作用域,就会从调用函数的作用域中找,结果就是2

    在《JavaScript权威指南》中有这样一个例子

    var scope = "global scope";
    function checkscope(){
     var scope = "local scope";
     function f(){
     return scope;
     }
     return f();
    }
    checkscope();
    
    var scope = "global scope";
    function checkscope(){
     var scope = "local scope";
     function f(){
     return scope;
     }
     return f;
    }
    checkscope()();
    

    这两段代码的执行结果其实都是“local scope”(因为其本质都是在执行f())
    根据词法作用域,所采用的变量是局部变量

    执行上下文栈

    可执行代码:有三种,函数代码,全局代码,eval代码
    当JavaScript执行到一个函数时,就会进行一定的准备工作(也叫执行上下文)
    JavaScript引擎创建了上下文栈(ECS)来方便地管理上下文
    让我们来模拟上下文执行地过程
    ECS = [] // 初始为空
    由于最先遇到的是全局代码globalContext,所以有ECS = [globalContext];
    并且会一直存在到程序结束
    如果此时遇到下面这段代码

    function fun3() {
     console.log('fun3')
    }
     
    function fun2() {
     fun3();
    }
     
    function fun1() {
     fun2();
    }
     
    fun1();
    
    

    工作原理:当执行一个函数时,就会创建一个执行上下文,并且压入执行上下文栈,当函数执行完毕时,就会将执行上下文从栈中弹出
    相当于:ECS:[globalContext,fun1,fun2,fun3] ------------> ECS:[glovalContext];
    只有当调用一个函数时才会创建上下文
    再来看两个例子

    var foo = function () {
     
     console.log('foo1');
     
    }
     
    foo(); // foo1
     
    var foo = function () {
     
     console.log('foo2');
     
    }
     
    foo(); // foo2
    
    
    function foo() {
     
     console.log('foo1');
     
    }
     
    foo(); // foo2
     
    function foo() {
     
     console.log('foo2');
     
    }
     
    foo(); // foo2
    

    由于JavaScript执行代码是一段一段地执行,并且会优先提取定义的函数式语句并执行
    在第二个例子中,第二次声明覆盖了第一次声明,所以都会输出foo2
    如果对其中一个进行变量提升,那么结果也会发生改变,这里不再赘述

    变量对象

    全局上下文

    全局上下文中的全局变量指的就是全局对象
    在客户端JavaScript中,全局对象就是Windows对象

    函数上下文

    在函数上下文中,用活动变量表示变量对象(AO)
    即在进入函数上下文后,变量对象才会变成活动对象

    执行过程

    当进入执行上下文时,这时候还没有执行代码
    AO是进入函数上下文时被创建的,它通过函数的arguments进行初始化
    会包含函数的所有形参,变量声明,函数声明
    如遇到下面代码时

    function foo(a) {
     var b = 2;
     function c() {}
     var d = function() {};
     
     b = 3;
     
    }
     
    foo(1);
    

    执行该函数上下文时,这时候的AO是

    AO = {
     arguments: {
     0: 1,
     length: 1
     },
     a: 1,
     b: undefined,
     c: reference to function c(){},
     d: undefined
    }
    

    代码执行阶段会顺序执行代码,执行完后是

    AO = {
     arguments: {
     0: 1,
     length: 1
     },
     a: 1,
     b: 3,
     c: reference to function c(){},
     d: reference to FunctionExpression "d"
    }
    

    总结

    1. 全局上下文的变量对象初始化是全局对象
    2. 函数上下文变量对象的初始化是argument对象
    3. 在进入函数上下文后添加形参,变量声明,函数声明的属性值
    4. 在执行代码阶段,会再次修改变量对象属性值
    console.log(foo);
     
    function foo(){
     console.log("foo");
    }
     
    var foo = 1;
    

    结果为函数对象,因为在执行上下文时,首先会处理函数声明,然后才处理变量声明,如果之前已经有声明过的变量,则不会发生覆盖

    作用域链

    作用域链指的是由多个变量对象创建的链表
    当查找变量对象时,会优先从当前上下文的变量中查找,如果找不到,会到父级去找(词法作用域)

    函数创建

    函数内部有一个属性scope,当函数被创建时,会保存所有的父级对象到其中
    scope可以表示所有父变量对象的层级链
    但是并不代表所有的作用域链

    函数激活

    函数激活时,进入函数上下文,创建活动变量后,添加到作用域链的顶端
    接下来用一个例子来帮助理解

    var scope = "global scope";
    function checkscope(){
     var scope2 = 'local scope';
     return scope2;
    }
    checkscope();
    

    执行过程如下:

    1. 函数被创建,保存作用域链到内部属性
    checkscope.[[scope]] = [
     globalContext.VO
    ];
    

    可见,此时作用域链内是全局对象
    2.函数执行上下文被压入上下文栈

    ECSstack = [
    Checkscope,
    globalContext
    ]
    

    3.函数并不立即执行,而是开始做准备工作,
    复制函数scope属性创建作用域链
    Scope:checkscope.[[scope]]
    用arguments创建活动对象,随后初始化活动对象

    AO = {
          arguments:{
                   length:0;
            },
           scope2:undefined,
           Scope:checkscope.[[scope]]
    }
    

    4.将活动对象压入作用域链顶端
    Scope:checkscope.[AO,[scope]]
    5.准备工作完成,开始执行函数,并且修改AO的值
    6.查找到scope2的值,函数返回后结束执行,并从ECS栈中弹出
    ECSstack = [globalContext]

    从ECMAScript规范解读this

    ECMAScript的中文版地址是(http://yanhaijing.com/es5/#115)
    ECMAScript有语言类型和规范类型两种类型
    语言类型就是开发者可以可以直接操作的,比如:undefined,null,string,number等等类型
    而规范类型是用算法描述ECMAScript语言结构和语言类型的
    接下来主要介绍规范类型中的Reference

    Reference

    根据ECMAScript里所述,Reference是用来解释delete,typeof以及赋值等操作行为的
    尤大是这么说的

    这里的 Reference 是一个 Specification Type,也就是 “只存在于规范里的抽象类型”。它们是为了更好地描述语言的底层行为逻辑才存在的,但并不存在于实际的 js 代码中.
    Reference 有三个组成部分

    1. base value 2. reference name 3. strict reference
      其中base value 就是属性所在的对象或者EnvironmentRecord,reference name是属性的名称
      下面举两个例子
    var foo = 1;
     
    // 对应的Reference是:
    var fooReference = {
     base: EnvironmentRecord,
     name: 'foo',
     strict: false
    };
    
    
    var foo = {
     bar: function () {
     return this;
     }
    };
     
    foo.bar(); // foo
     
    // bar对应的Reference是:
    var BarReference = {
     base: foo,
     propertyName: 'bar',
     strict: false
    };
    

    利用getbase可以得到reference的base value,getvalue可以得到该属性具体的值
    IsPropertyReference:如果base value是一个对象,返回true

    关于this

    我们来看看在函数调用的时候,如何确定this的取值
    从规范中可以得知如下

    1. 计算MemberExpression的结果赋值给ref
    2. 判断ref是否是一个Reference类型
    • 如果 ref 是 Reference,并且 IsPropertyReference(ref) 是 true, 那么 this 的值为 GetBase(ref)
    • 如果 ref 是 Reference,并且 base value值是 Environment Record, 那么this的值为 ImplicitThisValue(ref)
    • 如果 ref 不是 Reference,那么 this 的值为 undefined
    function foo() {
     console.log(this)
    }
     
    foo(); // MemberExpression 是 foo
     
    function foo() {
     return function() {
     console.log(this)
     }
    }
     
    foo()(); // MemberExpression 是 foo()
     
    var foo = {
     bar: function () {
     return this;
     }
    }
     
    foo.bar(); // MemberExpression 是 foo.bar
    

    原来对MemberExpression的描述就不多赘述,可以简单理解为()左边的部分

    var value = 1;
     
    var foo = {
     value: 2,
     bar: function () {
     return this.value;
     }
    }
     
    //示例1
    console.log(foo.bar());
    //示例2
    console.log((foo.bar)());
    //示例3
    console.log((foo.bar = foo.bar)());
    //示例4
    console.log((false || foo.bar)());
    //示例5
    console.log((foo.bar, foo.bar)());
    可以看到示例1MemberExpression是foo.bar,是一个函数
    reference是
    var Reference = {
     base: foo,
     name: 'bar',
     strict: false
    };
    

    可以看到它是第一种情况,this应该指向的是 GetBase(ref),也就是foo,答案为2
    对于示例2,加了括号并不会产生影响,所以结果不变
    至于示例3,4,5,他们都用了操作符,最后的结果是一个值,所以不是reference,this指向undefined

    还有一种情况,就是第二种情况,这时返回的是ImplicitThisValue(ref),该函数总是返回undefined,所以最后this也是指向undefined的(当然个人认为这句话还是有点问题)
    例子

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

    像上面这段代码在本机的输出结果其实是windows全局对象
    这是因为当前环境的JavaScript没有使用严格模式
    使用严格模式后,值为undefined

    执行上下文

    那么在了解清楚前面几个东西之后,就可以来看看执行上下文了
    对执行上下文来说,有3个重要的属性:
    1.变量对象 2.作用域链 3.this
    依然给出这个例子

    var scope = "global scope";
    function checkscope(){
     var scope = "local scope";
     function f(){
     return scope;
     }
     return f();
    }
    checkscope();
    

    现在我们来通过上下文的角度重新分析一下这段代码

    1. 创建全局上下文,压入上下文栈:ECSstack = [globalContext]
    2. 全局上下文初始化
      globalContext = {
      VO: [global],
      Scope: [globalContext.VO],
      this: globalContext.VO
      }

    初始化的同时,checkscope函数被创建,并保存作用域链到内部属性
    Checkscope.[[scope]] = {
    globalContext.VO
    }

    1. checkscope执行上下文入栈
      ECStack = [
      checkscopeContext,
      globalContext,
      ];

    复制函数[[ scope ]]属性创建作用域链
    用argument创建活动对象AO
    初始化活动对象,加入形参,函数声明,变量声明
    将活动对象压入作用域链顶端

    checkscopeContext = {
    AO: {
    arguments: {
    length: 0
    },
    scope: undefined,
    f: reference to function f(){}
    },
    Scope: [AO, globalContext.VO],
    this: undefined
    }
    

    在初始化的同时,保存作用域链到f的内部属性 [[ scope ]]

    1. 创建f函数执行上下文,f函数被压入上下文栈
      ECStack = [
      fContext,
      checkscopeContext,
      globalContext
      ];

    2. f函数上下文初始化,跟之前那一步一样

    	 fContext = {
    	 AO: {
    	 arguments: {
    	 length: 0
    	 }
    	 },
    	 Scope: [AO, checkscopeContext.AO, globalContext.VO],
    	 this: undefined
    	 }
    

    后面就是函数执行完赋值弹出出栈的过程

    闭包

    一般来说,闭包指的是函数+函数所能访问的自由变量
    自由变量是除了函数参数和函数中的局部变量,可以在函数中使用的变量
    在ECMAScript中,闭包指的是
    理论上:所有的函数。因为在创建函数时,其上下文的数据就都被保存起来了,函数在访问全局变量时其实就是在访问自由变量
    实践上:即使创建它的上下文已经摧毁,它依然存在(比如内部函数从父函数返回)
    在代码中引用了自由变量
    引入之前的一个例子

    var scope = "global scope";
    function checkscope(){
     var scope = "local scope";
     function f(){
     return scope;
     }
     return f;
    }
     
    var foo = checkscope();
    foo();
    

    在这个例子中,我们可以复习一下之前学习的执行上下文

    • 进入全局代码,创建全局执行上下文,全局执行上下文压入执行上下文栈
    • 全局执行上下文初始化
    • 执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 执行上下文被压入执行上下文栈
    • checkscope 执行上下文初始化,创建变量对象、作用域链、this等
    • checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出
    • 执行 f 函数,创建 f 函数执行上下文,f 执行上下文被压入执行上下文栈
    • f 执行上下文初始化,创建变量对象、作用域链、this等
    • f 函数执行完毕,f 函数上下文从执行上下文栈中弹出

    可以发现执行f时,checkscope其实已经被销毁了(出栈了)
    但是f还是可以通过作用域链找到对应的AO,所以即使checkscopeContext被销毁了,但是JavaScript却能让其AO一直在内存中,这就是实践中的闭包

    两个例子:

    var data = [];
     
    for (var i = 0; i < 3; i++) {
     data[i] = function () {
     console.log(i);
     };
    }
     
    data[0]();
    data[1]();
    data[2]();
    
    var data = [];
     
    for (var i = 0; i < 3; i++) {
     data[i] = (function (i) {
     return function(){
     console.log(i);
     }
     })(i);
    }
     
    data[0]();
    data[1]();
    data[2]();
    

    第一段代码的输出都是3,而第二段代码输出分别为0,1,2
    主要的区别就是第二段代码中多了个匿名函数的作用域链,大家可以自行去解读

    call,bind浅析

    call

    var foo = {
     value: 1
    };
    
    function bar() {
     console.log(this.value);
    }
    
    bar.call(foo); // 1
    

    可以看出,call函数改变了this的指向(指向了foo),并且bar函数也执行了
    当foo为null时,视为指向window

    bind

    bind会创建一个新函数,bind的第一个参数会作为它运行时的this

    var foo = {
     value: 1
    };
     
    function bar() {
     console.log(this.value);
    }
     
    // 返回了一个函数
    var bindFoo = bar.bind(foo); 
     
    bindFoo(); // 1
    

    类数组对象和arguments

    类数组对象

    指拥有一个length属性和若干索引属性的对象

    var array = ['name', 'age', 'sex'];
     
    var arrayLike = {
     0: 'name',
     1: 'age',
     2: 'sex',
     length: 3
    }
    

    可以发现类数组对象和数组的长度,遍历,读写一样
    但是类数组对象是不能用数组的方法的
    但是类数组可以通过各种方法转化成数组

    arguments

    function foo(name, age, sex) {
     console.log(arguments);
    }
     
    foo('name', 'age', 'sex')
    

    img
    在之前的介绍中我们其实已经对argument有了一定的了解

    length

    arguments的length表示实参的个数
    它所对应函数的length表示形参的个数

    callee

    通过该属性函数可以调用自身

    var data = [];
     
    for (var i = 0; i < 3; i++) {
     (data[i] = function () {
     console.log(arguments.callee.i) 
    }).i = i;
    }
     
    data[0]();
    data[1]();
    data[2]();
     
    // 0
    // 1
    // 2
    

    非严格模式下,实参和argument的值会共享(绑定)
    严格模式下,实参和argument的值不会共享

    继承的多种方式以及优缺点

    原型链继承

    Function Perent()
    {
        this.name = 'kevin'
    }
    
    Perent.prototype.getName() = function(){
    Console.log(this.name)
    }
    function Child () {
    }
     
    Child.prototype = new Parent();
    
    var child1 = new Child();
     
    console.log(child1.getName()) // kevin
    
    

    引用类型的属性会被所有实例共享,并且不能向Perent传参

    构造函数继承

    function Parent () {
     this.names = ['kevin', 'daisy'];
    }
     
    function Child () {
     Parent.call(this);
    }
     
    var child1 = new Child();
     
    child1.names.push('yayu');
     
    console.log(child1.names); // ["kevin", "daisy", "yayu"]
     
    var child2 = new Child();
     
    console.log(child2.names); // ["kevin", "daisy"]
    

    解决了利用原型链继承的问题
    缺点:方法在构造函数中定义,每次创建实例都会创建一遍方法

    组合继承

    构造函数继承+原型链继承
    结合了两者的优点,是常见的继承方式

    原型式继承

    function createObj(o) {
     function F(){}
     F.prototype = o;
     return new F();
    }
    

    同样存在共享的问题,但是在给对象赋值时会优先添加值

    寄生式继承

    创建一个仅用于封装过程的函数

    function createObj (o) {
     var clone = Object.create(o);
     clone.sayName = function () {
     console.log('hi');
     }
     return clone;
    }
    

    寄生组合式继承

    function Parent (name) {
     this.name = name;
     this.colors = ['red', 'blue', 'green'];
    }
     
    Parent.prototype.getName = function () {
     console.log(this.name)
    }
     
    function Child (name, age) {
     Parent.call(this, name);
     this.age = age;
    }
     
    Child.prototype = new Parent();
     
    var child1 = new Child('kevin', '18');
     
    console.log(child1)
    
    

    可以发现其调用了两次父构造函数,一次是new perent,一次是new child
    为了避免重复的调用,可以这样做

    var F = function () {};
     
    F.prototype = Parent.prototype;
     
    Child.prototype = new F();
     
     
    var child1 = new Child('kevin', '18');
     
    console.log(child1);
    

    设置一个空对象作为跳板,即可减少父构造函数的调用
    封装过后就是

    function object(o) {
     function F() {}
     F.prototype = o;
     return new F();
    }
     
    function prototype(child, parent) {
     var prototype = object(parent.prototype);
     prototype.constructor = child;
     child.prototype = prototype;
    }
    

    当要使用的时候,就prototype(Child, Parent);
    开发人员普遍认为寄生组合式继承是引用类型比较理想的继承范式

  • 相关阅读:
    从软件工程师角度聊聊 Kubernetes
    使用rpm包制作本地镜像仓库和使用httpd发布镜像服务实现内网使用yum命令
    当NLP遇见RPA:搭建人与机器沟通的桥梁
    深入分析高性能互连点对点通信开销
    Python数据分析与机器学习20- 逻辑回归项目实战4-模型评估方法:混淆矩阵
    51单片机仿真软件 Proteus 8 Pro 安装步骤
    【Unity】 2D 游戏 库存模块实现
    Vue多环境打包配置,根据命令生成不同打包文件名
    我的创作纪念日
    数据结构与算法-----顺序表(链表篇)
  • 原文地址:https://www.cnblogs.com/Sun-Wind/p/16832978.html