稳扎稳打JavaScript(二)——图解对象内存模型

1. 什么是JS对象?

在JS中,对象是一组无序属性的集合。其中,属性可以是基本数据类型、引用类型、函数。如下面这个对象的例子:

var chai={
    name : "柴毛毛", // 属性为基本数据类型
    perosn : { // 属性为引用类型
        address : "xxx",
        sex : "man"
    },
    getName : function () { // 属性为函数
        return this.name;
    }
}

也就是说,JS中的对象类似于Java中的Map,由键值对构成;其中键是字符串类型的属性名,值可以为上述三种类型中的任意类型。

2. 如何创建JS对象?

JS中创建对象的方法有很多,各有千秋。这篇博客主要介绍对象创建过程中的内存模型,因此只介绍通过构造函数创建对象的方法,其余方法期待下一篇博客吧。

2.1. 通过构造函数创建对象的步骤

  • 定义构造函数
function Person (name,age) {
    this.name = name;
    this.age = age;
    this.getName = function(){
        return name;
    }
}
  • 通过new创建对象
var person1 = new Person('柴毛毛',18);

通过上述两步后,就能创建一个Person对象,并由变量person1指向,通过该变量就能访问这个对象的所有属性。

构造函数与普通函数的区别?

3. 对象创建过程的内存模型

以上的概念较为基础,点到为止,为下面讲解对象的内存模型作铺垫,OK,重头戏来了。

3.1. 初始化构造函数

我们知道,构造函数也是函数,所有函数在被执行前都需要被初始化,函数初始化也就是创建该函数对象的过程。那么函数什么时候会被初始化呢?

当构造函数所在的外层函数被执行时,JS引擎会为该外层函数创建一个执行环境,并压入执行环境栈中。这个过程称为函数执行环境的准备阶段。 若构造函数以“函数声明”的方式定义,那么构造函数的初始化在外层函数的环境准备阶段完成; 若构造函数是以“函数表达式”的方式定义,那么只有当JS引擎执行到这一行代码的时候才会初始化构造函数。

我们知道,JS中的函数就是一个对象,因此函数的初始化过程其实就是创建了一个函数对象。

PS:不仅仅是构造函数,所有函数初始化发生的时机都是如此。 综上所述: 函数初始化的时机与函数所在的外层函数有关,也与函数的定义方式有关。 若函数以“函数声明”的形式定义,那么该函数的初始化在其外层函数执行前(即外层函数执行环境的准备阶段)完成; 若函数以函数表达式的形式定义,那么该函数的初始化在其外层函数执行到这行代码的时候完成。

3.2. 创建构造函数的原型对象

当构造函数的对象创建完成后,JS引擎随之会为该对象创建一个原型对象。 原型对象默认只有一个属性“construcor”,指向它的构造函数对象。 而每个函数对象默认都有一个prototype属性,该属性指向它的原型对象。 此时,内存中有如下对象:

PS:不仅仅是构造函数,所有函数初始化完成之后在内存中都有上述结构。 所有函数初始化完成后都会创建一个函数对象和一个原型对象,并且通过prototype、construcor属性相互引用。

3.3. 执行关键字new

var person1 = new Person('柴毛毛',18);

当通过关键字new创建一个对象时,JS引擎会做如下几件事:

  • 创建一个对象 就像这样:
var object = new Object();

这个object对象将会有一个指向原型对象的属性,它是一个隐式属性,我们无法通过JS代码访问,但JS引擎可以访问。 大多数浏览器将它定义为“proto”。

此时内存中一共存在三个对象,分别是:构造函数本身的函数对象、构造函数的原型对象、构造函数的实例对象。它们之间通过prototype、proto、constructor指针相互引用。

- 准备构造函数的执行环境(变量对象、作用域链、执行环境) 对象创建完后,就要开始执行构造函数中的代码。但在执行函数之前,首先需要准备函数的执行环境: (本过程详解请参阅:稳扎稳打JavaScript——作用域链) * 创建构造函数的变量对象(用于存储函数执行过程中的所有变量,包括this和arguments) * 创建构造函数的作用域,压入作用域链的头部,并且指向刚才创建的变量对象;(函数的作用域链在函数初始化时就已创建) * 创建构造函数的执行环境,并指向该函数的作用域链 此时内存模型如下:

到此为止,内存中存在两大块内容,一块是用于执行构造函数的“构造函数执行环境”,它包括:构造函数的执行环境、构造函数的作用域链、构造函数的变量对象。 另一块内存空间存储了与创建对象相关的内容,包括:构造函数本身的函数对象、构造函数的原型对象、构造函数的实例对象。 但此时这两块内存间并没有联系,接下来this就要出场了。

  • 将变量对象中的this指向实例对象 此时,构造函数的变量对象中的this指向了构造函数对象,从而使两大块内存之间建立起了桥梁,此时的内存图如下:
  • 执行构造函数中的代码
function Person (name,age) {
    this.name = name;
    this.age = age;
    this.getName = function(){
        return name;
    }
}

此时,JS引擎将会逐行执行构造函数中的代码。 比如,当代码执行到this.name=name时,JS引擎首先会在当前作用域链中寻找变量this。我们知道,查找变量首先从作用域的头部开始,因此首先寻找下标为0的变量对象;在该变量对象中找到了this,因此查找成功,紧接着在this指向的对象中创建name属性,并赋上局部变量name的值。

当构造函数中的代码执行完毕,内存模型如下:

构造函数创建的那个对象中多出了几个属性。

  • 返回刚创建的对象 最后,将this指向的那个对象返回。

4. 变量查找 与 属性查找 的区别

先来复习下JS的变量查找过程(本过程详解请参阅:稳扎稳打JavaScript——作用域链)。 仍以上述代码为例:

function Person (name,age) {
    this.name = name;
    this.age = age;
    this.getName = function(){
        return name;
    }
}
var person1 = new Person('柴毛毛',18);

4.1. 变量查找(作用域查找)

我们知道,通过构造函数创建一个对象时,会发生以下几件事: 1. 创建一个object对象 2. 准备构造函数的执行环境 3. 将构造函数的this指向object对象 4. 执行构造函数 5. 返回object对象 也就是说,通过new创建一个对象本质上仍然是把构造函数当作普通函数执行,只不过在构造函数执行前增加了一句var object=new Object();并且在构造函数执行结束后返回这个object;其余过程就是在执行一个普通函数。

那么在函数执行过程中,如果需要用到局部变量,就会发生变量查找。 当执行代码“this.name = name”时,这句代码涉及到两个局部变量:this、name。 以查找this为例,JS引擎会沿着当前函数的作用域链依次查找变量对象。若在某一个作用域指向的变量对象中找到this,则查找成功;否则就继续沿着作用域链向后查找。

4.2. 属性查找(原型链查找)

变量查找是在作用域链上进行,而属性查找是在原型链上进行的。

继续上述例子。当JS引擎在某个变量对象中找到this后,变量查找的过程结束,若查找的是一个基本数据类型的变量,那么查找结束;若查找的变量是一个对象,并且需要对该对象的属性进行操作,那么接下来就要进入属性查找的过程。

我们知道,构造函数执行前会将它的this指向构造函数的实例对象,因此,当执行“this.name”时,JS引擎就会在this指向的实例对象中查找。若在this指向的实例对象中查找不到,就会继续查找该实例对象proto属性指向的原型对象,若该原型对象中没有,则继续查找原型对象的原型对象,直到查找成功或找到Object的原型对象为止。

5. this是在函数执行时赋值的

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏柠檬先生

es6 Set 和Map 数据结构

ES6提供了新的数据结构Set,它类似于数组,但是成员的值都是唯一的,没有重复的值。   Set 本身是一个数据结构,用来生成Set 数据结构。     con...

1758
来自专栏https://www.cnblogs.com/L

【Scala篇】--Scala中集合数组,list,set,map,元祖

备注:数组方法 1     def apply( x: T, xs: T* ): Array[T] 创建指定对象 T 的数组, T 的值可以是 Unit, Do...

1191
来自专栏Java Edge

Spring Data JPA踩坑记:与Bootstrap Table的分页协作@PersistenceContext和@Resource的区别

注入一堆保存实体类状态的数据结构,针对实体类的不同状态(四种,managedh或detached等)可以做出不同的反应(merge,persist等等),其实就...

931
来自专栏wym

运算符重载(超详细)

C++中预定义的运算符的操作对象只能是基本数据类型。但实际上,对于许多用户自定义类型(例如类),也需要类似的运算操作。这时就必须在C++中重新定义这些运算符,赋...

692
来自专栏Golang语言社区

go语言中json转成map结构

package main import ( "encoding/json" "fmt" ) //把请求包定义成一个结构体 type Reques...

3085
来自专栏用户3030674的专栏

java类集框架(ArrayList,LinkedList,Vector区别)

主要分三类:集合(set)、列表(List)、映射(Map) 1.集合:没有重复对象,没有特定排序方式 2.列表:对象按索引位置排序,可以有重复对象 3.映射...

912
来自专栏开发与安全

比较显式调用构造函数和析构函数

1.首先看如下的代码,显式调用析构函数: #include <iostream> using namespace std; class MyClass { p...

18710
来自专栏我是攻城师

理解Java集合框架里面的的transient关键字

3448
来自专栏python学习之旅

Python笔记(十六):迭代器

    iterable对象(可迭代的对象):可以使用for循环,例如:字符串、列表 、字典 、集合等

592
来自专栏Play & Scala 技术分享

理解Scala的Symbol类型

2813

扫码关注云+社区