我们在面试中也好,实际的框架学习中也好,各种js 的书上js的原型,原型链是一个绕不过去的知识点,js就是基于原型设计的,所以我们要理解这个js的精髓的知识点是很重要的。那么我们就来一起看下是怎么肥四,
在说原型链之前先说下对象。在js中一切都是对象。我们来先看下这个对象。

Object 对象

对象是什么

面向对象编程(OOP):具有灵活性,代码可复用性,高度模块化等特点。
1.对象是单个实物的抽象。
2.对象是一个容器,封装了对应属性和方法。 属性是对象的状态,方法是对象的行为。

javascript 对象分为 普通对象,和函数对象

  • 普通对象
    最普通的对象:有proto属性(指向其原型链),没有prototype属性
    你真的了解javascript中的原型/原型链吗? - 图1

原型对象还有constructor属性(指向构造函数对象)

  • 函数对象
    只要是new Function() 创建的对象都是函数,函数对象同时有proto ,prototype 属性(指向原型对象),Object、Function、Array、Date、String、自定义函数都是

你真的了解javascript中的原型/原型链吗? - 图2

有一个特殊的地方就是Function 的prototype是原型对象也是函数对象

你真的了解javascript中的原型/原型链吗? - 图3

了解了对象,我们在看下什么是原型;

原型

原型指构造函数的prototype属性指向的对象;
每个 实例对象 都有一个 constructor 属性,指向它的构造函数

构造函数

需要一个模版(表示一类事物的共同特征),让对象生成。 类(class)就是对象的模版。
js不是基于类的,而是基于构造函数(constructor)和原型链(prototype)。
构造函数特点: 1.函数体内使用this关键字,代表了所要生成的对象实例。 2.生成对象,必须使用new 关键字实例化。
下面通过代码看下:

  1. function Test(name){
  2. this.name=name;
  3. }
  4. var t=new Test('zhangsan');
  5. var str=new String('lisi');
  6. console.log(t.constructor === Test); //true
  7. console.log(str.constructor === String); //true

每个 函数对象(包括构造函数) 都有一个 prototype 属性,指向函数的原型对象,这个 原型对象 的 constructor 属性,指向函数本身;

constructor属性

每个对象在创建时都会自带一个构造函数constructor属性;

  1. function Test(name){
  2. this.name=name;
  3. }
  4. var t=new Test('zhangsan');
  5. var str=new String('lisi');
  6. console.log(Test.prototype.constructor === Test);
  7. console.log(String.prototype.constructor === String);

每个 对象 都有一个 [[prototype]] 私有属性,指向它的构造函数的原型对象,但这个属性是不允许访问的
某些浏览器(例如 Chrome)提供 __proto__ 属性用于访问 [[prototype]] 私有属性;

  1. function Test(name){
  2. this.name=name;
  3. }
  4. var t=new Test('zhangsan');
  5. var str=new String('lisi');
  6. console.log(t.__proto__ === Test.prototype);
  7. console.log(str.__proto__ === String.prototype);
  8. console.log(Test.__proto__ === Function.prototype);
  9. console.log(String.__proto__ === Function.prototype);

构造函数的 constructor 属性都是指向 Function__proto__ 属性都是指向 Function.prototype
因为构造函数都是通过 new Function 来创建的,它们都是 Function 的实例对象,包括 Function 和 Object;

  1. function Test(name){
  2. this.name=name;
  3. }
  4. var t=new Test('zhangsan');
  5. var str=new String('lisi');
  6. console.log(Test.constructor === Function); //构造函数的 constructor 属性都是指向 Function
  7. console.log(Test.__proto__ === Function.prototype); //对象[[prototype]] 私有属性,指向它的构造函数的原型对象属性都是指向 Function.prototype
  8. console.log(String.constructor === Function);
  9. console.log(String.__proto__ === Function.prototype); //内置对象的
  10. console.log(Function.constructor === Function); //Function的构造函数的constructor也指向 Function
  11. console.log(Function.__proto__ === Function.prototype); //Function的私有属性 也同样指向Function.prototype
  12. console.log(Object.constructor === Function); //Object的构造函数的constructor也指向 Function
  13. console.log(Object.__proto__ === Function.prototype); //Object的私有属性 也同样指向Function.prototype

Object 外,其它构造函数的 prototype 属性的 __proto__ 属性都是指向 Object.prototype
Objectprototype 属性的 __proto__ 属性指向 null

  1. function Test(name){
  2. this.name=name;
  3. }
  4. var t=new Test('zhangsan');
  5. var str=new String('lisi');
  6. console.log(Test.prototype.__proto__ === Object.prototype)
  7. console.log(String.prototype.__proto__ === Object.prototype)
  8. console.log(Function.prototype.__proto__ === Object.prototype)
  9. console.log(Object.prototype.__proto__ === null)

原型对象

  1. function Test(name){
  2. this.name=name;
  3. }
  4. Test.prototype.foo = "bar"; // add a property onto the prototype
  5. var t = new Test();
  6. Test.prop = "some value"; // add a property onto the object
  7. console.log( t );
  8. // 结果如下
  9. {
  10. prop: "some value",
  11. __proto__: {
  12. foo: "bar",
  13. constructor: ƒ Test(),
  14. __proto__: {
  15. constructor: ƒ Object(),
  16. hasOwnProperty: ƒ hasOwnProperty(),
  17. isPrototypeOf: ƒ isPrototypeOf(),
  18. propertyIsEnumerable: ƒ propertyIsEnumerable(),
  19. toLocaleString: ƒ toLocaleString(),
  20. toString: ƒ toString(),
  21. valueOf: ƒ valueOf()
  22. }
  23. }
  24. }

如上所示, Test 中的__proto__是 Test.prototype.
但这是做什么的呢?当你访问 t 中的一个属性,浏览器首先会查看t 中是否存在这个属性。
如果 t 不包含属性信息, 那么浏览器会在 t 的 __proto__ 中进行查找(同 Test.prototype). 如属性在 t 的 __proto__ 中查找到,则使用 t 中__proto__ 的属性。

否则,如果 t 中 __proto__ 不具有该属性,则检查t 的__proto____proto__ 是否具有该属性。
默认情况下,任何函数的原型属性 __proto__ 都是 window.Object.prototype.
因此, 通过t 的 __proto____proto__ ( 同 Test.prototype__proto__ (同 Object.prototype)) 来查找要搜索的属性。

如果属性不存在 t 的 __proto____proto__ 中, 那么就会在t 的 __proto____proto____proto__ 中查找。
然而, 这里存在个问题:t 的 __proto____proto____proto__ 其实不存在。
因此,只有这样,在 __proto__ 的整个原型链被查看之后,这里没有更多的 __proto__ , 浏览器断言该属性不存在,并给出属性值为 undefined 的结论。

如下:

  1. function Test(){}
  2. Test.prototype.foo = "bar";
  3. var t = new Test();
  4. t.prop = "some value";
  5. console.log("t.prop: " + t.prop);
  6. console.log("t.foo: " + t.foo);
  7. console.log("Test.prop: " + Test.prop);
  8. console.log("Test.foo: " + Test.foo);
  9. console.log("Test.prototype.prop: " + Test.prototype.prop);
  10. console.log("Test.prototype.foo: " + Test.prototype.foo);
  11. //结果如下
  12. t.prop: some value
  13. t.foo: bar
  14. Test.prop: undefined
  15. Test.foo: undefined
  16. Test.prototype.prop: undefined
  17. Test.prototype.foo: bar

对上面的概念是不是有点晕,不急,我们来张图理解下,实例对象,构造函数,原型对象会更清晰
image.png

prototype属性的作用

js继承机制:通过原型对象实现继承。 原型对象的作用就是定义所有实例对象共享的属性和方法。

  1. function Test(){}
  2. Test.prototype.foo = "bar";
  3. var t = new Test();
  4. Test.prototype.foo = "par";
  5. var t1 = new Test();
  6. console.log(t.foo); //bar
  7. console.log(t1.foo); //par

原型链

原型链

上面我们了解到,每个对象都有 __proto__ 属性,并且这个 __proto__ 属性指向一个原型对象
因为原型对象也是对象,这个原型对象也有 __proto__ 属性,我们把这种关系称为原型链
image.png

属性访问

当需要访问一个对象的属性时,首先从该对象开始查找,如果能够找到,那么到此返回;
如果没有找到,就在该对象的 __proto__ 属性指向的原型对象中继续查找,如果能够找到,那么到此返回;
如果没有还没找到,那么一直往上查找原型对象,直至 __proto__ 属性指向 null,也就是原型链的顶端
若原型链上的所有原型对象都没有该属性,则返回 undefined

  1. function Test(name) {
  2. this.name = name;
  3. }
  4. Test.prototype.getName = function(){ console.log(this.name); }
  5. var t = new Test("zhangsan");
  6. var a= t.getName(); // 在 Test.prototype 中找到
  7. var b= t.toString(); // 在 Object.prototype 中找到
  8. var c= t.saiHello(); // 在原型链中无法找到
  9. console.log(a);
  10. console.log(b);
  11. console.log(c);

那我们为什么要使用原型呢?

在JavaScript 中继承是基于原型实现的,原型的作用在于不同实例对象之间可以共享数据,节省内存。

  1. function Test(name) {
  2. this.name = name;
  3. }
  4. Test.prototype.getName = function(){ console.log(this.name); }
  5. var t = new Test("zhangsan");
  6. var t1= new Test("lisi");
  7. // 不同实例对象 t 和 t1 都可以使用在 Test.prototype 上定义的 getName(共享数据)
  8. // 从而避免在每个实例对象上都要重复定义 getName(节约内存)
  9. var name1 = t.getName();
  10. var name2 = t1.getName();
  11. console.log(name1); // zhangsan
  12. console.log(name2); // lisi

V8中的JS Object结构

V8里面所有的数据类型的祖先类都是Object,Object派生HeapObject,提供存储基本功能,往下的JSReceiver用于原型查找,再往下的JSObject就是JS里面的Object,Array/Function/Date等继承于JSObject。左边的FixedArray是实际存储数据的地方。
image.png
在JS语言中就是通过点方式法来访问对象的属性和方法,此时对象的名字就是一个命名空间,而”.”之后紧接的是一个标识符,这个标识符一般为属性的名字。
而命名空间是可以支持链式调用的,也就是父命名空间后可以定义子命名空间,通过多个点符号来访问内部的命名空间。而另外一种访问方式,则是通过括号表示法,括号里面的内容是表示属性名的字符串,也可以是一个表达式方式来访问(ES6新增语法),可以支持动态属性名。
由于JS对象是可以支持动态设置属性,而在程序中对于对象的读取的频率是很高的,而由于对象的属性是存储在堆中,每次都需要去查找,因此V8引擎根据这种情况,通过快属性和慢属性的存储策略来提高对象属性的查找效率。
其次是对象的存储,由于V8引擎中,对象是存储在其管理下的堆内存中,在栈中存储的是表示对应堆内存的内存地址,也就是指针。在V8中,对象主要是由三个指针构成,分别是隐藏类,Property和Element组成的。如下图所示:
image.png
V8对象存储结构

其中,隐藏类是用来描述对象的结构,Property和Element是用于存放对象的属性,它们主要的区别体现在是否能被索引。
每个对象里包含一个隐藏属性proto,这个隐藏属性是一个指针,指向另外一个对象

v8中js-objects实现
image.png
关注公众号 code本缘