07月23, 2017

聊聊JS与内存(二)

在上一篇聊聊js与内存(一)中,我主要和大家分享了JS的7种标准类型的存储方式。这一篇主要内容是结合对象原型来分析一下构造函数和实例在内存中的存储情况。

对象属性类型

ECMA-262定义这些属性时是为了实现JS引擎用人,因此在JS中不能直接访问他们。

数据属性:

属性名称 定义 default值
[[ configurable ]] 表示能否通过delete删除属性从而重新定义属性,或能否修改属性为访问器属性 true
[[ Enumerable ]] 表示能否通过for-in 循环返回属性 true
[[ Writable ]] 表示 能否修改属性值 true
[[ Value ]] 包含这个属性的数据值 undefined

访问器属性:

属性名称 定义 default值
[[ configurable ]] 表示能否通过delete删除属性从而重新定义属性,或能否修改属性为访问器属性 true
[[ Enumerable ]] 表示能否通过for-in 循环返回属性 true
[[ Get ]] 在读取属性时调用 undefined
[[Set ]] 在写入属性时调用 undefined

要修改属性默认特性是,必须使用ECMAScript 5中的Object.defineProperty()方法。

用法如下:

alt

创建对象

一般来说,创建对象的方式:

  • Object 模式
  • 工厂模式

  • 构造函数模式

  • 通过 Function 对象实现

  • prototype 模式

  • 构造函数与原型方式的混合模式

  • 动态原型模式

  • 混合工厂模式

    各种方式各有千秋,这里先不赘述,大家有兴趣可以自己了解一下哦。

    本文中创建实例使用的是构造函数模式,也就是使用new的方式来声明,创建构造函数采用构造函数与原型方式的混合模式,接下来看具体内容。

    原型对象与构造函数存储关系

    每一个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针

    这句话是什么意思呢?我们举个例子来说:

    /*声明一个构造函数*/
    function Foo(){
        this.name = 'Jack';
        this.sayName = function(){
            console.log(this.name);
        }
        // this.sayName = new Function("console.log(this.name)")
    }
    Foo.prototype.friend = "Nic"
    

    从上面代码可以看到,我们声明了一个构造函数,这个构造函数自己带有一个原型对象(prototype)属性,这个prototype是object类型(实际上是继承自Object),在prototype中包含一个constructor函数,这个函数中包含一个指向Foo函数本身的指针(注意这里说的是包含,也就是说除了该指针constructor还有其他的属性)。

    我们可以尝试输出一下,看看打印结果:

    /*每个构造函数都有一个原型对象*/
    console.log(Foo.prototype)
    /*原型对象包含一个指针指向构造函数*/
    console.log(Foo.prototype.contructor === Foo)
    console.log(Foo.name)  //Foo
    

    alt

    接下来我们结合内存中的存储情况理解一下: alt

    *绿色背景不表示存储空间,是我用来说明当前存储的名称

    从上图可以看到,Foo函数在栈内存中存储的是一个引用地址,这个地址指向对内存中的Foo的存储区域,尽管Foo属于function类型,但function也属于object的一个子类,所以Foo构造函数也是通过堆内存进行存储的。

    alt *ECMAScript

    同理,Foo.prototype也是对象类型,也是通过堆进行存储的。不同的是构造函数本身和prototype存储分别指向不同的内存空间中,所以prototype中的constructor函数的指针指向Foo本身就成为Foo与prototype的纽带了。

    这里还需要注意一点,大家有没有注意到Foo.name的值是Foo而不是我们之前赋值的Jack,这是因为首先在声明Foo的时候系统并不会为我们写在函数内部的自定义属性值(如:this.name)单独开辟内存空间,这里你可以理解成所有属性只是以字符串的形式存储在堆内存中Foo函数的存储空间里。其次,大家可以看到截图中constructor中的属性包含name,说明name为function的内部属性,用来表示当前构造函数的名称,构造函数内部属性我们是没办法直接改写的,所以当我们输出时,实际是获取函数的内部属性name的值。

    关于这个构造函数与普通函数怎么区别呢?

    红皮书上讲,如果一个函数的使用方式是通过new的方式实例化对象,那就称为构造函数。如果使用方式是通过执行函数的方式,那就称为普通函数。

    构造函数与实例存储关系

    接下来,我们讲讲实例。一般我们通过new的方式创建实例。

    实例都包含一个指向原型对象的指针

    talk is cheap,show u the code.

    /*声明两个实例*/
    var f1 = new Foo();
    var f2 = new Foo();
    

    我们输出f1的内容,可以看到: alt

    f1的属性中包含____proto____属性,这个就是引言中所说的指向原型的指针。我们可以看看它的内容,它包含一个constructor函数,constructor包含指向Foo函数的指针。同时,可以看到f1中还包含name和sayName的属性,这个是用过Foo函数继承而来的。接下来,我们来看看内存中的存储情况:

    alt

    *绿色背景不表示存储空间,是我用来说明当前存储的名称

    上图中我们可以看到,栈内存中,分别存储了Foo,f1,f2对象的内存地址,指向了不同的堆内存的存储空间。但是Foo的prototype属性和f1,f2的proto属性在栈内存的存储地址是同一个,也就是说,实例的proto属性的指针实际是指向构造函数的prototype的存储空间。

    接下来我们来进行验证:

    console.log(Foo.prototype === f1.__proto__)  //true
    

    那么当我们改变属性值的时候,会发生什么情况呢?

    Foo.prototype.age = 23;
    console.log(f1.__proto__.age);    //23
    console.log(f2.__proto__.age);    //23
    f1.name = 'Linda';
    console.log(f2.name); //Jack
    

    可以看到当我改变了Foo.prototype属性时,相应实例中指向Foo.prototype__proto__的值也发生了改变。同理,当改变f1中__proto__中的属性值时,Foo.prototype和f2的__proto__属性也会发生改变。

    当我们改变f1的name属性时,发现f2的name属性值并没有发生改变,因为f1,f2本身存储在独立的内存空间中。

    最后要补充说明一点,我们重新举个例子:

    function Foo(){
        this.name = 'Jack';
        this.sayName = function(){
            console.log(this.name);
        }
    }
    
    var bar1 = new Foo();
    var bar2 = new Foo();
    
    console.log(bar1.name == bar2.name)   //true
    console.log(bar1.sayName == bar2.sayName)  //false
    

    为什么name属性值相等,但是sayName属性值不同呢?这是因为首先我们进行的是值与值的比较,获取到name的值均为"Jack"这个字符串,但是当我们获取sayName时,其实获取到的是function类型的对象,创建新实例会导致不同的作用于链和标识符解析,当然创建Function新实例的机制还是相同的,所以会输出false。

    ES6中class

    ES6中的类表达式的设计初衷是为了声明相应变量或传入函数作为参数。我们可以简单的认为类其实就是我们所说的构造函数。OK ,我们举个例子来说明:

    function PersonType(name){
        this.name = name;
    }
    
    PersonType.prototype.sayName = function(){
        console.log(this.name)
    }
    
    var person = new PersonType('Jack');
    person.sayName();   //Jack
    

    上述代码呢,是用我们ES5的方式实现的,接下来把它等价转换为ES6的方式:

    class PersonClass{
        //等价于PersonType构造函数
        constructor(name){
            this.name = name;
        }
        //等价于PersonType.prototype.dayName
        sayName() {
            console.log(this.name)       
        }
    }
    
    let person = new PersonClass("Jack")
    person.sayName();   //Jack
    

    你可以发现typeof PersonClass返回的结果与typeof PersonType一致都是function。所以实际上,类声明仅仅是基于已有的自定义类型声明的语法糖。

    需要注意的是,类的使用方式与函数还是有一定的不同之处的:

  • 类属性不可被赋予新值,PersonClass.prototype就是一个只读属性。

  • 函数声明可以被提升,而类声明与let const声明一样不可以被提升,也就是说在实际使用中可能会出现temporal dead zone。

  • 类中的左右的方法都不可以枚举(在自定义类型中,Object.defineProperty()方法指定的方法不可枚举)。

  • 每个类都包含[[construct]]内部方法,通过new来调用不含[[construct]]的方法会导致程序报错。

  • 使用除关键字new以外的方式调用类会导致程序报错。

  • 在类中修改类名会导致程序报错。

    好了,就这么多了!以上!

  • 本文链接:https://www.imwineki.cn/post/jstype2.html

    -- EOF --

    Comments

    评论加载中...

    注:如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理。