Javascript的原型链


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);
}

这样很简单,但是有一个问题:浪费了很多内存。

  1. 行走、哭泣这两个动作对于每个person其实是一样的,只需要各自引用同一个函数就可以了,没必要重复创建 100 个行走、100个哭……
  2. 这些person的性别都是一样的,没必要创建 100 次。
  3. 只有 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 关键字,那么可以少做四件事情:

  1. 不用创建临时对象,因为 new 会帮你做(你使用「this」就可以访问到临时对象);
  2. 不用绑定原型,因为 new 会帮你做(new 为了知道原型在哪,所以指定原型的名字为 prototype);
  3. 不用 return 临时对象,因为 new 会帮你做;
  4. 不要给原型想名字了,因为 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等,就没有此属性。


Author: 顺坚
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source 顺坚 !
评论
 Previous
分区分表分库 分区分表分库
互联网时代,传统应用都有这样一个特点:访问量、数据量都比较小,单库单表都完全可以支撑整个业务。随着业务的发展和用户规模的迅速扩大,对系统的要求也越来越高。因此传统的MySQL单库单表架构的性能问题就暴露出来了。而有下面几个因素会影响数据库性
2022-02-09
Next 
并发编程AQS详解 并发编程AQS详解
AQS全名:AbstractQueuedSynchronizer,是并发容器J.U.C(java.util.concurrent)下locks包内的一个类。它实现了一个FIFO(FirstIn、FisrtOut先进先出)的队列。底层实现的数
2022-01-08
  TOC