本文中心:

这篇文章比较难懂,所以读起来比较晦涩。所以,我简单列一下提纲:

在第一部分,从函数原型开始谈起,目的是想搞明白,这个属性是什么,为什么存在,在创建对象的的时候起到了什么作用!

在第二部分,阅读的时候,请分清楚__proto__和内置对象的区别;搞清楚这点。然后,我们再一点点分析__proto__属性。

第三部分,本来不在我写作的范围,但是看到网上的很多文章在继承的时候,使用的方法五花八门。所以来谈一下,Object.create()这个方法的好处。

1.函数原型

1.1.函数特有的prototype属性

标题中所谓的特有,指的是只有函数才具有prototype属性,ECMAScript标准规定每个函数都拥有一个属于自己的原型(prototype)。

那么这个函数的原型到底是什么,它又有什么用呢?

  • 函数的原型是什么?

    用代码证明函数原型是一个对象。

    console.log(typeof Object.prototype); //"object",   这里用到了Object()函数。
    console.log(Object.prototype instanceof Object) //true

    从上面的输出结果中,我们得出函数的原型是一个对象。那么,这个对象本身有什么属性呢?
    我们知道,任何一个对象都具有最基本的方法,比如 toString().valueof()...既然函数原型是对象类型,那么它肯定也具有这些基本的方法...
    所以这些方法是从哪里来的呢?要想搞清楚这些,那么我们就必须要从Object()的原型谈起!
           
        上面这幅图片,帮我们认清楚了Object()函数的原型,这个函数原型本身不具有任何属性,但是其具有一些很基本的方法,这些方法有什么用,这里暂且不论。但是到目前为此,请记住一点:函数原型是一个对象。因为只有知道了最基本的这一点,我们下面的讨论才具有意义。

  • 函数原型有什么用?
      只是知道函数原型是一个对象,才只是开始,因为我们想知道的是:函数的原型对象什么用,为什么要要设计这么个东东!!
    看下面的一段代码,我们跟着代码来分析:
    var object = new Object();  //new 一个对象
    console.log(object.toString()); //输出这个对象,firefox控制台下输出结果 [obejct object] 

    这里出现了一个很奇怪的现象...object对象没有toString()函数,这里在输出的时候为什么不报错呢? ok! 看下面一副图片:
            
    在object这个对象中,其具有一个__proto__属性,这个属性是哪里来的? ......等等......有没有觉得__proto__的值和Object.prototype的值时惊人的相似呢。难道这是巧合吗, 还是说他们本来就是同一个对象呢!!!我们来测试一下:

    var object = new Object();
    console.log(object.__proto__==Object.prototype); //true.

    事实再一次证明,世上没那么多的巧合!!object.__ptoto__和Object.prototype真的指向的是同一个对象。

    现在我们解决了一个问题,就是object.toSring()这个函数,真正的来源是Object.prototype。那么object对象为什么能访问Object.prototype中的方法呢...要回答这个问题,需要弄清楚两件事情:
    第一,当 new Object()到底发生了什么?
    第二:__proto__这个属性起到了什么作用?
    要想弄明白上面的两个问题,我们依然需要分析程序

    var object=new Object(); 

    当执行这句话的时候,解释器帮我们干了三样活:
         (1).开辟了一些内存空间给object;
            (2).将this指针指向object(暂且不论这点,this指针我们以后也会开单题来说).Ok.现在知道了new有什么用了;
            (3).将object添加一个内置属性属性,__proto__的值和内置属性的值总是相等的;
    知道了当new Object()的时候,解释器帮我们给对象添加了一个内置属性,接下来解决第二个问题,内置属性[[__proto__]]有什么用?
    看下面的代码

    var object = new Object();
    
    var proto1 = {};
    proto1.name = '张三'
    object.__proto__ = proto1;
    console.log(object.name); //张三 var proto2 = {};
    proto2.name = '李四'
    object.__proto__ = proto2;
    console.log(object.name); //李四 

    在上面的例子中,object一直没有变,但是其属性__proto__是指向的对象变了。根据上例,我们可以得出结论,对象是可以访问到属性__proto__指向的对象所拥有的变量的,而且使用的时候就像是其自己的属性一样。

    总结以上,我们可以得出结论:
        每个函数都拥有一个属于自己的原型,这个原型的实质是一个对象,当该函数被当做构造函数使用(即new调用)的时候,所生成的实例会有一个内置的属性,当访问这个对象的时候,如果在实例中没有找到对应属性,则会根据内置属性,查找内置属性所指向的对象,一直到最上层若找不到则返回undefined.(严格模式的时候会报错).

1.2.函数原型prototype的constructor属性

在创建一个新的函数的时候,这个函数的原型中会有一个constructor属性,那么这个属性是否有存在的意义呢?

看下面的一段代码:

var Person=function(){};
console.log(Person.prototype.constructor); //function constructor是一个函数
console.log(Person.prototype.constructor===Person);//true Person.prototype.constructor===Person

上述代码,证明了constructor这个属性是真实存在的,且这个属性的值初始化为构造函数本身。那么这个属性有什么很重要的意义吗? 再看下面的码:

var Person = function () {
};
var xiaoming = new Person();
console.log(xiaoming instanceof Person); //true
Person.prototype.constructor = null;
console.log(xiaoming instanceof Person); //true

由上面例子可以得出,constructor属性只是标识原型是归属于哪个函数的,这个属性虽然是解释器给我们默认的,但是相对来说没有那么重要,甚至说起来可以是一点用处都没有。对于一个函数,在刚创建的时候总是这个样子的。

1.3prototype的宿命---用于继承

有些事情在你出生的那一刻就已经注定要发生。prototype在出生之初就已经注定其宿命。下面让我们来谈谈这所谓的宿命吧!!

根据1.1部分,我们知道函数的原型,在函数实例化的时候会被赋值给实例的内置属性的。假设有两个类A和B,代码如下:

//A函数如下
var A = function (a) {
this.a = a;
}
A.prototype.getA = function () {
return this.a;
}
// B函数如下
var B = function (a, b) {
A.call(this, a); //借用A类构造函数,很重要的一点!!!
this.b = b;
}
B.prototype.getB = function () {
return this.b;
}

A和B分别是两个类的构造函数,他们此时在内存中的结构如下图所示:

现在如果我们想让B类成为A的子类,该如何做呢? 首先,我们应该认识到一点,如果B是A的子类,那么B就应该能访问A中的属性和方法。父类A中有属性a和方法getA(),那么子类B中也应该有属性a且能访问方法getA();如果我们能实现如下图所示的结构是否就能做到B类继承A类呢?

与上图相比,仅仅修改了B.prototype中的【【__proto__】】.然后一切的一切都自然而然的发生了。总之,子类B为了继承A做了两样活: 子类B类通过A.call();这一步借用A的构造函数拥有的A类的变量,又通过修改原型中的【【__proto__】】才做到能访问A类中的函数..想到这里不得不说一句,好伟大的设计。如果只是为了实现继承,有N多种方法能实现,但是请注意,如果考虑内存中的分配情况以及效率和程序的健壮性,那么就只有一个函数能够完美的做到图中所示的那样。这个函数就是Object.create()函数,这个函数的宿命就是为了实现继承。

为什么这么说呢,请看第二部分慢慢解析!!

2.__proto__属性和内置属性的区别

2.1.你真的了解__proto__这个属性吗?

如果你认为自己很了解这个属性,那么请思考以下几个问题?

1.这个属性是什么性质的属性? 访问器属性 or 数据属性?

2. 这个属性存在在哪里? 是每个对象都有,还是在整个内存中仅有一份。

3.这个属性与内置属性有什么关系?

如果你对上面的上个问题很困惑,或者你认为__proto__就是内置属性的话,那么我们还是花一点时间正正三观吧。

2.1.1.证明1:__proto__是访问器属性

看下面一段代码:

var descriptor=Object.getOwnPropertyDescriptor(Object.prototype,"__proto__");
console.log(descriptor); //输出结果:
configurable true
enumerable false
get function() { [native code] }
set function() { [native code] }

看到上面的输出结果,你是否已经接受了__proto__就是一个访问器属性呢....如果你还不相信..那么接着看,这只是践踏你世界观的开始!!!

2.1.2.证明2:__proto__属性在内存中仅存一份

  从证明1的输出结果中,我们知道configurable=true;这也就告诉我们这个对象是可以被删除的...下面看一段代码:

var object={};
var result=delete object.__proto__; console.log(result); //true
console.log(typeof object.__proto__) //object.

  请回答? 为什么显示删除成功了, typeof object.__proto__还是输出 object呢?

ok!!  要理解透彻这些,我们插入一些delete运算符的知识.

ECMAScript规定:delete 运算符删除对以前定义的对象属性或方法的引用,而且delete 运算符不能删除开发者未定义的属性和方法。

那么什么情况下,delete会起作用呢?

delelte运算如果想正常操作必须满足三个条件: 1,该属性是我们写的,即该属性存在。2.删除的是一个对象的属性或方法。3.该属性在配置时是可以删除的,即(configurable=true)的情况下可以删除。

那么,上面的例题中,返回值为true.,它符合上面的三个条件吗?

对于1,该属性我们是可以访问的,所以,证明该属性存在。

对于2,__proto__是某个对象的属性。

对于3:因为 configurable=true,所以也是符合的。

ok!上面的三点都符合,在返回值等于true的情况下,删除还是失败了呢! 因为还有下面一种情况,就是在对象上删除一个根本不存在的于自身的属性也会返回true!

var object = {
};
Object.prototype.x={};
var result = delete object.x;
console.log(result); //true.console.log(object.x);//object
 

看到没有,这两个例子在输出结果上很相似呢?因为 __proto__属性存在于该对象上的原型上面,所以,该对象可以访问。但是不能删除该属性。如果想删除该属性,那么请在Object.prototype上删除。这个保证能删除成功。

为了证实这一点,我们再看一下

var object={};
console.log(typeof object.__proto__); //object
delete Object.prototype.__proto__;
console.log(typeof object.__proto__); //undefined 删除成功。

我们可以发现,在Object.prototype删除__proto__属性后。object上也无法访问了。这是因为,所以对象都有一个共同的原型Object.prototype.在这个上面删除__proto__,那么所有的对象也都不具有这个__proto__属性了。

这也就证明了,内存中仅存一份__proto__属性,且该属性定义在Object.prototype对象上。

2.1.3.这个属性与内置属性有什么关系

从某种程度上来说,__proto__如果存在,那么它总是等于该对象的内置属性。而且在上一篇文章中我们也点出了一点,改变__proto__的指向也能改变内置属性的指向。所以,如果你固执的把__proto__认为就是内置对象,那也无可厚非。

但是请记住两点:

1. 内置对象不可见,但是内置对象总是存在的。

2.__proto__如果存在,那么它的值就是内置对象,但是这个__proto__并不总是存在的。

如果你一定认为__proto__就是内置对象,也可以,但是请保证两点:不要在程序的任何地方用__proto__属性。或者,如果你一定要用__proto__这个属性的话,请保证永远不要修改Object.prototype中的__proto__!!

如果你不能保证这两点,请远离__proto__.因为,一旦有人不遵守约定的话,这个bug的危害代价太大。比如,下面这样...

var A = function () {
}
A.prototype.say = function () {
return 'hello';
}
var B = function () {
}
//子类继承父类
function extend(subClass, superClass) {
var object = {
};
object.__proto__ = superClass.prototype;
subClass.prototype = object;
subClass.prototype.constructor = subClass;
}
extend(B, A); //B继承A
var b = new B();
b.say();

上面是一段,毫无问题的代码...但是如果有一个小白用户,在某一天执行了下面一句代码,

var A = function () {
}
A.prototype.say = function () {
return 'hello';
}
var B = function () {
} function extend(subClass, superClass) {
var object = {
};
object.__proto__ = superClass.prototype;
subClass.prototype = object;
subClass.prototype.constructor = subClass;
} delete Object.prototype.__proto__; //或则其他的等等 extend(B, A);
var b = new B();
b.say(); //TypeError: b.say is not a function 报错...如果是这种错误,调试起来肯定会让你欲哭无泪的。所以,如果你想写出好的程序,请远离__proto__.

2.2.Object.create()应运而生

时无英雄,使竖子成名! JavaScript的今天的盛行,可以说就是这句话的写照。Object.create()也是这样,在继承时并不是我们非用它不可,只是在排除了使用__proto__之后,除了使用这个函数之外,我们没有其他更好的选择。

这个函数在W3C中这个函数是怎么定义的呢?

Object.create 函数 (JavaScript)

创建一个具有指定原型且可选择性地包含指定属性的对象。

Object.create(prototype, descriptors)
prototype

必需。 要用作原型的对象。 可以为 null。

descriptors

可选。 包含一个或多个属性描述符的 JavaScript 对象。

“数据属性”是可获取且可设置值的属性。 数据属性描述符包含 value 特性,以及 writable、enumerable 和 configurable 特性。 如果未指定最后三个特性,则它们默认为false。 只要检索或设置该值,“访问器属性”就会调用用户提供的函数。 访问器属性描述符包含 set 特性和/或 get 特性。 有关详细信息,请参阅 Object.defineProperty 函数 (JavaScript)

这是这个函数在W3C中的定义,我来举个例子来说明这个函数怎么用吧!!!

var A = function (name) {
this.name = name;
};
A.prototype.getName = function () {
return this.name
}
var returnObject = Object.create(A.prototype, {
name: {
value: 'zhangsan',
configurable: true,
enumerable:false,
writable:true
}
});

上述代码运行完毕之后,returnObject在内存中的结构如图所示:

看看上面这张图,在类比1.3中的最后一张图,如下:

发现是不是,惊人的相似...所以,知道Objece.create()的强大了吧!! 我们分析过,下面这张图是实现继承的完美状态,而Object.create()就是为了做到这些,专业为继承而设计出来的函数。

下面是一段用Object.create()函数实现子类继承父类的代码;

//子类继承父类,这段代码在执行delete Object.prototype.__proto__;这段代码之后仍然可以正常运行。
function extend(subClass, superClass) {
var prototype=Object.create(superClass.prototype);
subClass.prototype =prototype;
subClass.prototype.constructor = subClass; }
 

var A = function () {
}
A.prototype.say = function () {
return 'hello';
}
var B = function () {
}

extend(B, A);
var b = new B();
b.say();  //hello

Ok!  我知道,你能用N多种方法实现继承,但是请记住,在继承的时候请不要用__proto__这个属性,因为它没你想象中俺么可靠。如果你想获得一个对象的原型,那么这个方法可以做到,Object.getPrototypeOf。与之对应的是Object.setPrototypeOf方法。

也许你也会说,Object.setPrototypeOf方法可以在远离__proto__的情况下实现继承啊啊...如果你在看到它的源代码你还会这么说吗?

Object.setPrototypeOf = Object.setPrototypeOf || function(obj, proto) {
obj.__proto_ _ = proto;    //也是用到了__proto__.
return obj;
}

总结:

整篇文章,从prototype谈起,分析了函数的prototype的类型与作用(这个大家都在谈)。

在第二部分,我们分析了__proto__,得到的结果,内置属性和__proto__根本不是一回事。__proto__这个属性不可靠..撇开,这个属性是非标准属性不说,这个属性隐藏的bug就能致人于死地,所以,在写程序时,请谨记一点,珍爱生命,远离__proto__.

在最后,我们浅谈了一下,用Object.create()实现继承的好处。这个部分,很难讲的清楚,需要慢慢去体会。

在下一篇中,我们会分析,为什么会说JS中一切皆是对象!。。。

最新文章

  1. 如何将C#类库做成COM
  2. Statement和PreparedStatement的区别; 什么是SQL注入,怎么防止SQL注入?
  3. linux下VMware安装出现的问题解决
  4. AutoCAD2007专业版
  5. sass、git、ruby的安装与使用。
  6. SVN客户端常用命令
  7. maven profile参数动态打入
  8. 潜水 java类加载器ClassLoader
  9. Spring(2)——Spring IoC 详解
  10. 安装 mongo 4.0
  11. ASPxGridView 用法
  12. 狼抓兔子 BZOJ- 1001 最小割
  13. canvas和图片互转
  14. tcpdump抓sql语句
  15. Struts2和MVC的简单整合
  16. Haskell中cabal install glib遇到的问题
  17. Linux Guard Service - 杀死守护进程
  18. 【linux】FTP添加用户,设置权限和目录
  19. Diff Two Arrays
  20. hdu 5800 To My Girlfriend(背包变形)

热门文章

  1. Bayesian CTR Prediction for Bing
  2. 在linux上安装rz、sz包
  3. asp.net提高程序性能的技巧(一)
  4. mysql中将时间转为秒
  5. 适配ios11与iphone x实践
  6. 平稳切换nginx版本
  7. Python学习手册 :Python 学习笔记第一天
  8. java spark-streaming接收TCP/Kafka数据
  9. Linux文件类型介绍
  10. App 监控、推广