JavaScript 对象 & 原型

分享

前言

这次的 why what or how 主题:JavaScript 对象 & 原型。

此类文章在百度上一搜一大把,其实不用再写了,但是本着把这个问题解释的清清楚楚明明白白,还是开始写了。

原因如下:

  1. 面试宝典类文章,弄张图片一糊弄,让人觉得自己理解了。
  2. 解释类文章,告诉你一堆语法,对语法一顿解释,告诉你就是这样的,还是没说清楚。
  3. 很少有文章单独解释这个点!但这个点是基础!真的很重要!

所以本篇文章想说一说对象 & 原型,但为了确保能顺利理解,请先看完 JS 变量存储?栈 & 堆?NONONO!,因为该篇文章从变量存储的角度来解释 JavaScript 对象 & 原型,请确保看完,在看这篇文章。

什么是对象?

既然要说清楚,先问一个最最基本的问题:什么是对象?

对象一句话就能解释清楚:

对象是一系列属性 & 数据的集合。

JavaScript 中创建一个对象的方式有很多,常见的如下:

// 字面量直接创建
let obj1 = {foo: 'bar'};

// 通过实例化 Object 类
let obj2 = new Object({foo: 'bar'});

class A {
    constructor(foo){
        this.foo = foo;
    }
}

// 通过自定义类创建
let obj3 = new A('bar');

以上代码最终都产生了 {foo: 'bar'} 这个对象。对象下有一个叫 foo 的属性,它的值为 bar。那么它在堆中是如何存储的呢?

JavaScript 下对象的存储

看过了 JS 变量存储?栈 & 堆?NONONO! 相信大家对于上图应该不陌生。

对象的取值

我们继续看一个较为复杂的对象(对象下某个属性也是一个对象):

let complexObj = {
    num: 1,
    str: 'string',
    obj: {
        foo: 'bar',
    }
}

在内存中的模型如下:

复杂对象模型

我们模拟一下 complexObj.obj.foo 这个取值过程。

复杂对象取值过程

取值过程:碰到存储的值是地址值时,就到相应的地址值继续进行操作。

对象,总的来说,知识点有两点:

  1. 创建对象:在内存堆中开辟一块用于存储一系列 属性 & 数据 的空间。
  2. 对象取值:根据存储的数据取值,如果是地址值,则到相应内存堆中继续进行操作。

对象到这里就差不多了,那原型又是什么呢?

原型 & 原型链

原型是对象下的一个属性,每个对象都有,同时原型也是一个对象。

文本的描述总是不直观的,我们通过代码来看:

let proto = {
    foo: 'bar'
}

let obj = {
    // ...
    __proto__: proto
}

设置对象 __proto__ 属性的过程,就是给对象设置了原型,同时该属性指向一个对象。

注: 当然 ES6 已经不建议这么做了,有专门的方法(setPrototypeOf)设置原型,为了解释方便,这里用 ES5 的代码作为示例。

有人可能会有疑问:既然是赋值操作,那应该也可以给对象的原型设置为非对象,为什么原型是对象呢?关于这个问题的答案,可以用以下代码测试:

let a = {};
a.__proto__ = 1;
console.log(a.__proto__);

复制到浏览器即可看到效果,将对象的原型设置为非对象这个操作,是被禁止的,也就是说你设置了也没用。

原型的定义也很简单,那么原型是干嘛用的呢?

原型的作用

原型补充了原对象,当需要访问对象中属性,但该属性又不存在时,就会去原型上寻找。

按照上面的例子,obj.foo 返回什么?以下伪代码就是寻值的过程:

function getValue(obj, attr){
    let searchObj = obj
    while(searchObj 下不存在 attr 属性){
        searchObj = obj 的原型
    }
    return searchObj 下的 attr 属性 
}

getValue(obj, 'foo')

以下为图示过程:

获取原型上的属性

根据上图(或是伪代码)我们可得知最终的结果是: bar

那如果对象上的原型还是没有该属性呢?

在原型的定义中,提到过:原型同时也是一个对象。转换一下思路,那么这个问题就变成了:如果对象上没有该属性那会发生什么?

去对象下的原型下找!对!我们刚说完!

这种层层递进的关系我们就把它称为:原型链!

到这你可能会问:如果按照这样一直找下去,那不是无穷无尽了?这就需要引出一个特殊的对象,可以在 Chrome 的控制台打印 Object.prototype 输出的对象,你可以仔细找找,该对象有没有原型?

OH NO! 竟然有原型,你个骗子。

不要激动,继续去查看它原型是什么:null

这和我之前说的:原型也是一个对象,有出入,因为 null 明显不是一个对象。

但是,typeof null 确实是 object,笔者也有猜想过是不是和这有关系。但猜想是猜想,为了语义的完整性,我们重新定义一下原型:

原型是对象下的一个属性,每个对象都有,同时原型是对象或是 null

同时定义一下原型链的特点:

原型链由一个个对象组成,原型链有终点,这个终点是 null

OK 既然原型链有了终点,那么如果一个属性在原对象和所有的原型链上都不存在的话,他的值是什么?

undefined(未定义)啊!

现在我们已经清楚原型和对象的关系,那如何设置对象的原型呢?

设置原型

其实上面已经提到过,我们可以直接设置对象的原型:

ES5 中

let proto = {
    foo: 'bar'
}

let obj = {
    // ...
    __proto__: proto
}

ES6 中

由于 __proto__ 是非标准属性,因此在 ES6 中建议使用 setPrototypeOf 设置对象的原型。

let obj = {};

let proto = {
    foo: 'bar'
}

Object.setPrototypeOf(obj, proto);

那如果一个对象还没有生成,比如仅仅定义了一个类,但又想控制通过这个类生成的对象的原型,该如何呢?

既然问了,那肯定是有的,解决方案就是函数的 prototype 属性。

prototype

我们都知道在 JavaScript 中函数也是一个对象,这个特殊对象下有一个 prototype 属性,是干嘛用的呢?

在使用 new 关键字调用该函数时,函数下的 prototype 属性所保存的对象就是生成对象的原型。

通过代码来解释:

function A(bar) {
    this.bar = bar;
}

A.prototype.test = function(){
    console.log('test');
}
A.prototype.testAttr = 'testAttr';

let a = new A('bar');

下面用伪代码来解释 new A('bar') 这个过程:

function fakeNew(A, bar){
    // 生成 this 为一个空对象。
    let this = {};
    Object.setPrototypeOf(this, A.prototype);
    
    // 执行 A 函数内的代码
    this.foo = bar;
    
    // 将 this 返回
    return this;
}

fakeNew(A, 'bar');

相信大家看完代码就能理解 prototype 这个属性的作用了,但这是 ES5 的代码,ES6 中并不建议直接写 prototype ,而直接使用 class,但其本质是一样的。复制下面代码到 Chrome 里即可查到真相:

class A {
    constructor(bar) {
        this.foo = bar;
    }
    test() {
        console.log('test');
    }
}

console.dir(A);
class 的 prototype

如上图所示,A 仍有 prototype 属性,并且定义中除了 constructor 函数,其他的函数都在 prototype 属性内。

可以认为是 ES5 function 语法糖吧,至少这里可以这么认为,但请不要认定,相比较于 ES5 function 还是有差别的,这块内容不属于本篇范畴,有机会单独写一篇讨论讨论。

对象的创建

绕了一圈,又绕了回来,现在我们回过头来看看对象的创建。

通过以上的阐述,我们知道了以下几点:

  1. 对象是一些列 属性 & 数据 的集合。
  2. 原型是对象下的一个属性,它的值是一个对象(或 null)。

那好,现在我在问一个问题,你用以下代码创建的对象,它的原型是什么?

let obj = {};

有点疑惑?因为它既没有主动添加原型,也不是从类创建的对象,那他的原型就没有了?

答案当然是否定的,只要你把这段代码贴到 Chrome 控制台就可以了。想要进一步知道这个对象到底哪儿来的,试试以下代码:

obj.__proto__ === Object.prototype; // true

很明显,这个新创建的对象为 Object 这个类的 prototype 属性,难不成这个 obj 是通过 Object 类创建的?

bingo ~ 你离真相又进了一步,在 JavaScript 中所有的对象都由 Object 所创建(PS:不管你用什么姿势!)。

好,对象的直接创建弄明白了,在来个间接创建的问题,请问以下代码所创建的 obj 的原型是什么?

function A (){};
let obj = new A();

当然是 Aprototype 啊,这还用问?那 Aprototype 又是什么呢?

放到 Chrome 下一看便知:

function 的 prototype

由上图可见,是一个简单的对象,最原始的 prototype 其中仅仅包含了 constructor__proto__

constructor 就是引用它自己。

A.prototype.constructor === A; // true

__proto__ 就是这个对象的原型,只要是一个对象,自然而然会有这个属性,那么这个属性的值是什么?

A.prototype.__proto__ === Object.prototype; // true

当然是 Object.prototype 啊,所有对象都由 Object 这个类所创建嘛!

小结

好了,一个简单的点,反反复复,回头一看,这么长了,原本不想写这么长的... 看来原理的东西虽说不难,但想解释清楚还是要点时间的。最后提几个问题当做是小结了吧:

  1. 对象是什么?
  2. 原型又是什么?
  3. 对象和原型的关系是什么?
  4. 原型和对象的关系又是什么?
  5. 原型如何创建,它的默认值是是嘛?
  6. 原型链是怎么组成的?
  7. 原型链的终点是啥子?

最后,大部分文章提到了原型(链)都会提到,继承。emmmm 先放过继承吧,继承只是编程的一种方式,不是原理性的东西,下次讲吧 ~~

最后的最后

该系列所有问题由 minimo 提出,爱你哟~~~

阅读更多

浏览器下的 Event Loop

前言 javascript 是以单线程的形式运行在宿主环境下,javascript 采用了回调的形式来解决异步任务。 为什么是单线程? javascript 的最开始的出现是为了给 web 页面增添一些动态的效果,那么就避免不了获取页面上的元素信息,如果 javascript 是以多线程的形式运行在浏览器内,如果两个线程内的 javascript 同时去获取/修改,某个页面上的元素,那么浏览器该让哪个 javascript 线程拥有获取/修改该元素的权限呢?由于元素的信息会经常性的发生变化,那么又改如何去同步各个线程内所保存的元素信息呢? 所以综合以上问题, javascript 是单线程的原因就显示意见了,单线程在执行时,对于元素信息的引用在同一时间仅可能只有一个,那么以上所有的问题都不存在了。 什么是异步任务? 任何代码在执行时,都会碰到一些需要经过大量时间运算或是等待的代码,在浏览器的环境下,常见的就是 http 任务,比如:资源的加载(图片的 onload 事件),ajax 的请求(XMLHttpRequest 的 onLoad 事件)还有页面元素的点击事件以及

By Breeze

node 下的 Event Loop

前言 通过 浏览器下的 Event Loop ,可以得知 javascript 是一门事件驱动的语言, javascript 主线程通过不断的调用事件队列中的事件来完成异步任务。那么 Node 下是否也是如此? Node 下的事件队列 在 Node 官网有这样一篇文章:The Node.js Event Loop, Timers, and process.nextTick() 。 该文章主要讲述了 Node 中是如何处理以及实现 Node 下的 Event Loop。 主要包含以下内容: 事件队列 不同于浏览器,Node 下的有 6 个事件处理阶段,其执行顺序和队列名称如下: ┌───────────────────────────┐ ┌─>│ timers │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ pending callbacks │ │ └─────────────┬─────────────┘ │

By Breeze

什么是 HTML 5?

前言 作为程序员,技术的落实与巩固是必要的,因此想到写个系列,名为 why what or how 每篇文章试图解释清楚一个问题。 这次的 why what or how 主题:现在几乎所有人都知道了 HTML5 ,那么 H5 到底相比于 HTML4 有什么区别呢? 升级版?标准版! HTML5 做为 HTML 标准的第 5 版,于 2014 年发布,相信关注过 HTML5 的发展史的朋友都知道该版本是 WHATWG 和 W3C 握手言和后诞生的,是 W3C 组织与浏览器厂商相互妥协的结果,其中的绝大部分规范都由 WHATWG 组织所制定,之后由 W3C 采纳并入标准中。

By Breeze

什么是 HTML?

前言 作为程序员,技术的落实与巩固是必要的,因此想到写个系列,名为 why what or how 每篇文章解释清楚一个问题。 作为 why what or how 的第一章,选一个较为简单的话题,什么是 HTML ? 释义 HTML - HyperText Markup Language,超文本标记语言 标记语言,一类以固定的形式描述文档结构或是数据处理细节的语言,一般为纯文本形式,其内容作为其他程序的输入。 因此,HTML 是一种用于描述网页的标记语言,纯文本,其内容作为浏览器的输入。浏览器会解析 HTML 文本内容,最终呈现页面。 那么超文本又是什么? 在网页出现前,人们的阅读习惯是从前往后,从上到下,就像阅读小说,必须从头到尾的看完,思维是单向的,并不存在岔路口。而超文本从字面上理解就是:超出了一般的阅读习惯,看文章的时候思维是发散的,

By Breeze