JS用了几年了,对于JS原型链的认知还比较模糊,JS是函数式编程语言,同时也可以是面向对象编程的语言(而且函数也是对象。这是与Java不同的地方),但尽管JS是有面向对象的特质,但其实现原理却Java有很大区别。因此不要用Java面向对象的思维方式去看待Javascript,不然会带来一些理解上的困惑。Javascript是一种基于原型Prototype的语言,而不是基于类的语言。在Javascript中,类和对象看起来没有太多的区别,先来看一下原型链的使用,然后再说它的原理。
原型链的使用
通常,这样创建一个对象:
function person(name){
this.sayHi = function(){
alert('hi ' + this.name);
}
this.name = name;
}
var p = new person("dan");
p.sayHi();
使用new关键字,通过对象(函数也是特殊对象)创建一个对象实例。在基于类的语言中,属性或字段通常都是在类中事先定义好了,但在Javascript中,在创建对象之后还可以为类添加字段。 如下
function Animal(){}
var cat = new Animal();
cat.color = "green";
color这个字段只属于当前的cat实例。对于后加的字段,如果想让animal的所有实例都拥有呢?使用Prototype
function Animal(){}
Animal.prototype.color= "green";
var cat = new Animal();
var dog = new Animal();
console.log(cat.color);//green
console.log(dog.color);//green
// 添加方法
Animal.prototype.run = funciton(){
console.log("run");
}
dog.run();
通过Prototype不仅可以添加字段,还可以添加方法。(实际上在JS中,不应该区分字段和方法,这是Java中的叫法。因为在JS中函数方法和对象是同等地位的,因此函数被当作普通的字段属性,可以被赋值给变量,可以当做参数传递等)
因此通过prototype属性,在创建对象之后还可以改变对象的行为。如下代码
Array.prototype.remove = function(elem){
var index = this.indexof(elem);
if(index >= 0){
this.splice(index, 1);
}
}
var arr = [1, 2, 3] ;
arr.remove(2);
当然这种改变JS API的方式是不应该做的,这里只是举个例子。Prototype允许我们在创建对象之后来改变对象或类的行为,并且这些通过prototype属性添加的字段或方法所有对象实例是共享的。这种为类动态地增加属性和方法可以被当作一种伪继承,但这种伪继承并非产生 了新的子类而是修改了原有的类,别着急,prototype属性还有另一种方式实现继承,代码如下
function Person(name, age)
{
this .name = name;
this .age = age;
this .show = function (){
var res = "我是 " + this .name + " 年龄 " + this .age + "." +this.gender;
return res;
};
}
// 给person添加几个属性
Person.prototype.gender = "女" ;
Person.prototype.getSex = function (){
return this .gender;
};
//定义学生对象
function Student(num){
this .num=num;
}
Student.prototype= new Person( "alice" ,23);
Student.prototype.constructor=Student
//Student.prototype= Person.prototype;
var s= new Student(123434);
console.log(s.show());
上例定义了Person类,增加了getSex()方法,又定义了Student类, 并将Student类的prototype属性设为Person对象。表明Student原型是Person对象,也就是Student继承了Person, 会得到其方法和属性
new关键字
在JS中new关键字做了什么,下面以一个例子来说明一下
var Person = {
idCard:1
gender: 'man',
height:60,
birth:'2020-01-01',
walk:function(){ /*走俩步的代码*/},
cry:function(){ /*狂奔的代码*/ },
}
如果需要制造 100 个人怎么办呢?循环 100 次吧:
var PersonList = []
var person
for(var i=0; i<100; i++){
person = {
idCard:1
gender: 'man',
height:60,
birth:'2020-01-01',
walk:function(){ /*走俩步的代码*/},
cry:function(){ /*狂奔的代码*/ },
}
PersonList.push(person);
}
这样很简单,但是有一个问题:浪费了很多内存。
- 行走、哭泣这两个动作对于每个person其实是一样的,只需要各自引用同一个函数就可以了,没必要重复创建 100 个行走、100个哭……
- 这些person的性别都是一样的,没必要创建 100 次。
- 只有 idCard 和身高需要创建 100 次,因为每个person有自己的 idCard 和身高。
用原型链可以解决重复创建的问题:我们先创建一个「person原型」,然后让「person」的 proto 指向「person原型」,这时代码如下
var BasePerson = {
gender: 'man',
birth:'2020-01-01',
walk:function(){ /*走俩步的代码*/},
cry:function(){ /*狂奔的代码*/ },
}
var PersonList = []
var person
for(var i=0; i<100; i++){
person = {
idCard: i, // ID 不能重复
height:42
}
/*实际工作中不要这样写,因为 __proto__ 不是标准属性*/
person.__proto__ = BasePerson ;
PersonList.push(person);
}
有人觉得创建一个士兵的代码分散在两个地方很不优雅,于是我们用一个函数把这两部分联系起来:
function Person(idCard){
var p = {}
p.__proto__ = Person.原型
p.idCard = idCard
p.height = 42
return p
}
Person.原型 = {
gender: 'man',
birth:'2020-01-01',
walk:function(){ /*走俩步的代码*/},
cry:function(){ /*狂奔的代码*/ },
}
// 保存为文件:Person.js
然后就可以愉快地引用「Person」来创建Person了:
var PersonList = []
for(var i=0; i<100; i++){
PersonList.push(Person(i))
}
JS 之父的关怀
JS 之父创建了 new 关键字,可以让我们少写几行代码:
function Person(idCard){
var p = {} // 1.我帮你创建临时对象
p.__proto__ = Person.原型 // 2.我帮你绑定原型
p.idCard = idCard
p.height = 42
return p // 3.我帮你return对象实例
}
Person.原型 = { // 4.统一叫做prototype
gender: 'man',
birth:'2020-01-01',
walk:function(){ /*走俩步的代码*/},
cry:function(){ /*狂奔的代码*/ },
}
只要你在士兵前面使用 new 关键字,那么可以少做四件事情:
- 不用创建临时对象,因为 new 会帮你做(你使用「this」就可以访问到临时对象);
- 不用绑定原型,因为 new 会帮你做(new 为了知道原型在哪,所以指定原型的名字为 prototype);
- 不用 return 临时对象,因为 new 会帮你做;
- 不要给原型想名字了,因为 new 指定名字为 prototype。
这一次我们用 new 来写
function Person(idCard){
var p = {}
p.__proto__ = Person.prototype
p.idCard = idCard
p.height = 42
return p
}
Person.prototype = {
gender: 'man',
birth:'2020-01-01',
walk:function(){ /*走俩步的代码*/},
cry:function(){ /*狂奔的代码*/ },
}
// 保存为文件:Person.js
然后是创建Person(加了一个 new 关键字):
var PersonList = []
for(var i=0; i<100; i++){
PersonList.push(new Person(i))
}
new 的作用,就是省那么几行代码。(也就是所谓的语法糖)
_proto__属性
_proto__
属性在js中是相当重要的概念,面向对象编程和委托设计都是围绕它展开的。但同时它在js的内部实现中,却十分的复杂,这里我们好好讨论下这个独特的属性。首先这个属性是什么?在官方的es5中,定义了一个名叫[[prototype]]的属性,每个对象都拥有这样一个属性,这个属性是一个指针,它指向一个名叫原型对象的内存堆。而原型对象也是对象,因此又含有自己的[[prototype]]的属性,又指向下一个原型对象。 那么终点是哪? 当然是我们的Object.prototype对象
注意,这里使用的是[[prototype]],而并非 _proto_
。那么这是一个东西吗?当然![[prototype]]是官方所定义的属性,而_proto__
是浏览器自己对[[prototype]]所做的实现。也就是说, _proto__
是浏览器自己根据标准制定出来的。
ECMA-262第五版的时候这个内部属性叫[Prototype]
,而_proto_
是Firefox,Chrome和Safari浏览器提供的一个属性,在其他的实现里面,这个内部属性是没法访问的,所以你能从控制台看到的是_proto_
,所以我觉得这个属性应该用[Prototype]
比较符合它的本质。
prototype属性
prototype对象的作用,就是定义所有实例对象共享的属性和方法,所以它也被称为实例对象的原型,而实例对象可以视作从prototype对象衍生出来的。通过new出来的实例是没有prototype属性的,只有函数才有prototype属性,这个属性值为一个object对象实例对象时没有这个属性的,实例对象通过__proto__
这个内部属性([[prototype]])来串起一个原型链的,通过这个原型链可以查找属性,方法。准确的说,只有构造函数才有prototype属性。通常我们自定义的函数都属于构造函数,所以都有此属性。JS运行时环境内置的函数有些不是构造函数,比如alert和Math.sqrt等,就没有此属性。