我们在面试中也好,实际的框架学习中也好,各种js 的书上js的原型,原型链是一个绕不过去的知识点,js就是基于原型设计的,所以我们要理解这个js的精髓的知识点是很重要的。那么我们就来一起看下是怎么肥四,
在说原型链之前先说下对象。在js中一切都是对象。我们来先看下这个对象。
Object 对象
对象是什么
面向对象编程(OOP):具有灵活性,代码可复用性,高度模块化等特点。
1.对象是单个实物的抽象。
2.对象是一个容器,封装了对应属性和方法。 属性是对象的状态,方法是对象的行为。
javascript 对象分为 普通对象,和函数对象
- 普通对象
最普通的对象:有proto属性(指向其原型链),没有prototype属性
原型对象还有constructor属性(指向构造函数对象)
- 函数对象
只要是new Function() 创建的对象都是函数,函数对象同时有proto ,prototype 属性(指向原型对象),Object、Function、Array、Date、String、自定义函数都是
有一个特殊的地方就是Function 的prototype是原型对象也是函数对象
原型
原型指构造函数的prototype属性指向的对象;
每个 实例对象 都有一个 constructor
属性,指向它的构造函数
构造函数
需要一个模版(表示一类事物的共同特征),让对象生成。 类(class)就是对象的模版。
js不是基于类的,而是基于构造函数(constructor)和原型链(prototype)。
构造函数特点: 1.函数体内使用this关键字,代表了所要生成的对象实例。 2.生成对象,必须使用new 关键字实例化。
下面通过代码看下:
function Test(name){
this.name=name;
}
var t=new Test('zhangsan');
var str=new String('lisi');
console.log(t.constructor === Test); //true
console.log(str.constructor === String); //true
每个 函数对象(包括构造函数) 都有一个 prototype
属性,指向函数的原型对象,这个 原型对象 的 constructor
属性,指向函数本身;
constructor属性
每个对象在创建时都会自带一个构造函数constructor属性;
function Test(name){
this.name=name;
}
var t=new Test('zhangsan');
var str=new String('lisi');
console.log(Test.prototype.constructor === Test);
console.log(String.prototype.constructor === String);
每个 对象 都有一个 [[prototype]]
私有属性,指向它的构造函数的原型对象,但这个属性是不允许访问的
某些浏览器(例如 Chrome)提供 __proto__
属性用于访问 [[prototype]]
私有属性;
function Test(name){
this.name=name;
}
var t=new Test('zhangsan');
var str=new String('lisi');
console.log(t.__proto__ === Test.prototype);
console.log(str.__proto__ === String.prototype);
console.log(Test.__proto__ === Function.prototype);
console.log(String.__proto__ === Function.prototype);
构造函数的 constructor
属性都是指向 Function
,__proto__
属性都是指向 Function.prototype
因为构造函数都是通过 new Function
来创建的,它们都是 Function 的实例对象,包括 Function 和 Object;
function Test(name){
this.name=name;
}
var t=new Test('zhangsan');
var str=new String('lisi');
console.log(Test.constructor === Function); //构造函数的 constructor 属性都是指向 Function
console.log(Test.__proto__ === Function.prototype); //对象[[prototype]] 私有属性,指向它的构造函数的原型对象属性都是指向 Function.prototype
console.log(String.constructor === Function);
console.log(String.__proto__ === Function.prototype); //内置对象的
console.log(Function.constructor === Function); //Function的构造函数的constructor也指向 Function
console.log(Function.__proto__ === Function.prototype); //Function的私有属性 也同样指向Function.prototype
console.log(Object.constructor === Function); //Object的构造函数的constructor也指向 Function
console.log(Object.__proto__ === Function.prototype); //Object的私有属性 也同样指向Function.prototype
除 Object
外,其它构造函数的 prototype
属性的 __proto__
属性都是指向 Object.prototype
而 Object
的 prototype
属性的 __proto__
属性指向 null
function Test(name){
this.name=name;
}
var t=new Test('zhangsan');
var str=new String('lisi');
console.log(Test.prototype.__proto__ === Object.prototype)
console.log(String.prototype.__proto__ === Object.prototype)
console.log(Function.prototype.__proto__ === Object.prototype)
console.log(Object.prototype.__proto__ === null)
原型对象
function Test(name){
this.name=name;
}
Test.prototype.foo = "bar"; // add a property onto the prototype
var t = new Test();
Test.prop = "some value"; // add a property onto the object
console.log( t );
// 结果如下
{
prop: "some value",
__proto__: {
foo: "bar",
constructor: ƒ Test(),
__proto__: {
constructor: ƒ Object(),
hasOwnProperty: ƒ hasOwnProperty(),
isPrototypeOf: ƒ isPrototypeOf(),
propertyIsEnumerable: ƒ propertyIsEnumerable(),
toLocaleString: ƒ toLocaleString(),
toString: ƒ toString(),
valueOf: ƒ valueOf()
}
}
}
如上所示, 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
的结论。
如下:
function Test(){}
Test.prototype.foo = "bar";
var t = new Test();
t.prop = "some value";
console.log("t.prop: " + t.prop);
console.log("t.foo: " + t.foo);
console.log("Test.prop: " + Test.prop);
console.log("Test.foo: " + Test.foo);
console.log("Test.prototype.prop: " + Test.prototype.prop);
console.log("Test.prototype.foo: " + Test.prototype.foo);
//结果如下
t.prop: some value
t.foo: bar
Test.prop: undefined
Test.foo: undefined
Test.prototype.prop: undefined
Test.prototype.foo: bar
对上面的概念是不是有点晕,不急,我们来张图理解下,实例对象,构造函数,原型对象会更清晰
prototype属性的作用
js继承机制:通过原型对象实现继承。 原型对象的作用就是定义所有实例对象共享的属性和方法。
function Test(){}
Test.prototype.foo = "bar";
var t = new Test();
Test.prototype.foo = "par";
var t1 = new Test();
console.log(t.foo); //bar
console.log(t1.foo); //par
原型链
原型链
上面我们了解到,每个对象都有 __proto__
属性,并且这个 __proto__
属性指向一个原型对象
因为原型对象也是对象,这个原型对象也有 __proto__
属性,我们把这种关系称为原型链
属性访问
当需要访问一个对象的属性时,首先从该对象开始查找,如果能够找到,那么到此返回;
如果没有找到,就在该对象的 __proto__
属性指向的原型对象中继续查找,如果能够找到,那么到此返回;
如果没有还没找到,那么一直往上查找原型对象,直至 __proto__
属性指向 null
,也就是原型链的顶端
若原型链上的所有原型对象都没有该属性,则返回 undefined
;
function Test(name) {
this.name = name;
}
Test.prototype.getName = function(){ console.log(this.name); }
var t = new Test("zhangsan");
var a= t.getName(); // 在 Test.prototype 中找到
var b= t.toString(); // 在 Object.prototype 中找到
var c= t.saiHello(); // 在原型链中无法找到
console.log(a);
console.log(b);
console.log(c);
那我们为什么要使用原型呢?
在JavaScript 中继承是基于原型实现的,原型的作用在于不同实例对象之间可以共享数据,节省内存。
function Test(name) {
this.name = name;
}
Test.prototype.getName = function(){ console.log(this.name); }
var t = new Test("zhangsan");
var t1= new Test("lisi");
// 不同实例对象 t 和 t1 都可以使用在 Test.prototype 上定义的 getName(共享数据)
// 从而避免在每个实例对象上都要重复定义 getName(节约内存)
var name1 = t.getName();
var name2 = t1.getName();
console.log(name1); // zhangsan
console.log(name2); // lisi
V8中的JS Object结构
V8里面所有的数据类型的祖先类都是Object,Object派生HeapObject,提供存储基本功能,往下的JSReceiver用于原型查找,再往下的JSObject就是JS里面的Object,Array/Function/Date等继承于JSObject。左边的FixedArray是实际存储数据的地方。
在JS语言中就是通过点方式法来访问对象的属性和方法,此时对象的名字就是一个命名空间,而”.”之后紧接的是一个标识符,这个标识符一般为属性的名字。
而命名空间是可以支持链式调用的,也就是父命名空间后可以定义子命名空间,通过多个点符号来访问内部的命名空间。而另外一种访问方式,则是通过括号表示法,括号里面的内容是表示属性名的字符串,也可以是一个表达式方式来访问(ES6新增语法),可以支持动态属性名。
由于JS对象是可以支持动态设置属性,而在程序中对于对象的读取的频率是很高的,而由于对象的属性是存储在堆中,每次都需要去查找,因此V8引擎根据这种情况,通过快属性和慢属性的存储策略来提高对象属性的查找效率。
其次是对象的存储,由于V8引擎中,对象是存储在其管理下的堆内存中,在栈中存储的是表示对应堆内存的内存地址,也就是指针。在V8中,对象主要是由三个指针构成,分别是隐藏类,Property和Element组成的。如下图所示:
V8对象存储结构
其中,隐藏类是用来描述对象的结构,Property和Element是用于存放对象的属性,它们主要的区别体现在是否能被索引。
每个对象里包含一个隐藏属性proto,这个隐藏属性是一个指针,指向另外一个对象
v8中js-objects实现
关注公众号 code本缘