Javascript中prototype属性的详解

原文链接:http://www.cnblogs.com/Uncle-Keith/p/5834289.html

在典型的面向对象的语言中,如java,都存在类(class)的概念,类就是对象的模板,对象就是类的实例。但是在Javascript语言体系中,是不存在类(Class)的概念的,javascript中不是基于‘类的’,而是通过构造函数(constructor)和原型链(prototype chains)实现的。但是在ES6中提供了更接近传统语言的写法,引入了Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。基本上,ES6的class可以看作只是一个语法糖,它的绝大部分功能,ES5都可以做到,新的class写法只是让原型对象的写法更加清晰、更像面向对象编程的语法而已。

  按照我的习惯,在写文章前我会给出文章目录。

  以下内容会分为如下小节:

  1.构造函数的简单介绍

  2.构造函数的缺点

  3.prototype属性的作用

  4.原型链(prototype chain)

  5.constructor属性

    5.1:constructor属性的作用

  6.instanceof运算符

1.构造函数的简单介绍

  在我的一篇Javascript 中构造函数与new命令的密切关系文章中,详细了介绍了构造函数的概念和特点,new命令的原理和用法等,如果对于构造函数不熟悉的同学,可以前往细细品味。以下做一个简单的回顾。

  所谓构造函数,就是提供了一个生成对象的模板并描述对象的基本结构的函数。一个构造函数,可以生成多个对象,每个对象都有相同的结构。总的来说,构造函数就是对象的模板,对象就是构造函数的实例。

  构造函数的特点有:

    a:构造函数的函数名首字母必须大写。

    b:内部使用this对象,来指向将要生成的对象实例。

    c:使用new操作符来调用构造函数,并返回对象实例。

  看一个最简单的一个例子。

1     function Person(){
2         this.name = ‘keith‘;
3     }
4
5     var boy = new Person();
6     console.log(boy.name);    //‘keith‘

2.构造函数的缺点

  所有的实例对象都可以继承构造函数中的属性和方法。但是,同一个对象实例之间,无法共享属性。

 1     function Person(name,height){
 2         this.name=name;
 3         this.height=height;
 4         this.hobby=function(){
 5             return ‘watching movies‘;
 6         }
 7     }
 8
 9     var boy=new Person(‘keith‘,180);
10     var girl=new Person(‘rascal‘,153);
11
12     console.log(boy.name);    //‘keith‘
13     console.log(girl.name);    //‘rascal‘
14     console.log(boy.hobby===girl.hobby);  //false

  上面代码中,一个构造函数Person生成了两个对象实例boy和girl,并且有两个属性和一个方法。但是,它们的hobby方法是不一样的。也就是说,每当你使用new来调用构造函数放回一个对象实例的时候,都会创建一个hobby方法。这既没有必要,又浪费资源,因为所有hobby方法都是童颜的行为,完全可以被两个对象实例共享。

  所以,构造函数的缺点就是:同一个构造函数的对象实例之间无法共享属性或方法。

3.prototype属性的作用

  为了解决构造函数的对象实例之间无法共享属性的缺点,js提供了prototype属性。

  js中每个数据类型都是对象(除了null和undefined),而每个对象都继承自另外一个对象,后者称为“原型”(prototype)对象,只有null除外,它没有自己的原型对象。

  原型对象上的所有属性和方法,都会被对象实例所共享

  通过构造函数生成对象实例时,会将对象实例的原型指向构造函数的prototype属性。每一个构造函数都有一个prototype属性,这个属性就是对象实例的原型对象。

 1     function Person(name,height){
 2         this.name=name;
 3         this.height=height;
 4     }
 5
 6     Person.prototype.hobby=function(){
 7         return ‘watching movies‘;
 8     }
 9
10     var boy=new Person(‘keith‘,180);
11     var girl=new Person(‘rascal‘,153);
12
13     console.log(boy.name);    //‘keith‘
14     console.log(girl.name);    //‘rascal‘
15     console.log(boy.hobby===girl.hobby);  //true

  上面代码中,如果将hobby方法放在原型对象上,那么两个实例对象都共享着同一个方法。我希望大家都能理解的是,对于构造函数来说,prototype是作为构造函数的属性;对于对象实例来说,prototype是对象实例的原型对象。所以prototype即是属性,又是对象。

  原型对象的属性不是对象实例的属性。对象实例的属性是继承自构造函数定义的属性,因为构造函数内部有一个this关键字来指向将要生成的对象实例。对象实例的属性,其实就是构造函数内部定义的属性。只要修改原型对象上的属性和方法,变动就会立刻体现在所有对象实例上。

1     Person.prototype.hobby=function(){
2         return ‘swimming‘;
3     }
4     console.log(boy.hobby===girl.hobby);  //true
5     console.log(boy.hobby());    //‘swimming‘
6     console.log(girl.hobby());    //‘swimming‘

  上面代码中,当修改了原型对象的hobby方法之后,两个对象实例都发生了变化。这是因为对象实例其实是没有hobby方法,都是读取原型对象的hobby方法。也就是说,当某个对象实例没有该属性和方法时,就会到原型对象上去查找。如果实例对象自身有某个属性或方法,就不会去原型对象上查找。

1     boy.hobby=function(){
2         return ‘play basketball‘;
3     }
4     console.log(boy.hobby());    //‘play basketball‘
5     console.log(girl.hobby());    //‘swimming‘

  上面代码中,boy对象实例的hobby方法修改时,就不会在继承原型对象上的hobby方法了。不过girl仍然会继承原型对象的方法。

  总结一下:

  a:原型对象的作用,就是定义所有对象实例所共享的属性和方法。

  b:prototype,对于构造函数来说,它是一个属性;对于对象实例来说,它是一个原型对象。

  

4.原型链(prototype chains)

  对象的属性和方法,有可能是定义在自身,也有可能是定义在它的原型对象。由于原型对象本身对于对象实例来说也是对象,它也有自己的原型,所以形成了一条原型链(prototype chain)。比如,a对象是b对象的原型,b对象是c对象的原型,以此类推。所有一切的对象的原型顶端,都是Object.prototype,即Object构造函数的prototype属性指向的那个对象。

  当然,Object.prototype对象也有自己的原型对象,那就是没有任何属性和方法的null对象,而null对象没有自己的原型。

1     console.log(Object.getPrototypeOf(Object.prototype));    //null
2     console.log(Person.prototype.isPrototypeOf(boy))    //true

  原型链(prototype chain)的特点有:

    a:读取对象的某个属性时,JavaScript引擎先寻找对象本身的属性,如果找不到,就到它的原型去找,如果还是找不到,就到原型的原型去找。如果直到最顶层的Object.prototype还是找不到,则返回undefined

    b:如果对象自身和它的原型,都定义了一个同名属性,那么优先读取对象自身的属性,这叫做“覆盖”(overiding)。

    c:一级级向上在原型链寻找某个属性,对性能是有影响的。所寻找的属性在越上层的原型对象,对性能的影响越大。如果寻找某个不存在的属性,将会遍历整个原型链。

  看概念可能比较晦涩,我们来看一个例子。但是理解了概念真的很重要。

1     var arr=[1,2,3];
2     console.log(arr.length);    //3
3     console.log(arr.valueOf())    //[1,2,3]
4     console.log(arr.join(‘|‘))    //1|2|3    

  上面代码中,定了一个数组arr,数组里面有三个元素。我们并没有给数组添加任何属性和方法,可是却在调用length,join(),valueOf()时,却不会报错。

  length属性是继承自Array.prototype的,属于原型对象上的一个属性。join方法也是继承自Array.prototype的,属于原型对象上的一个方法。这两个方法是所有数组所共享的。当实例对象上没有这个length属性时,就会去原型对象查找。

  valueOf方法是继承自Object.prototype的。首先,arr数组是没有valueOf方法的,所以就到原型对象Array.prototype查找。然后,发现Array.prototype对象上没有valueOf方法。最后,再到它的原型对象Object.prototype查找。

  来看看Array.prototype对象和Object.prototype对象分别有什么属性和方法。

1     console.log(Object.getOwnPropertyNames(Array.prototype))
2 //["length", "toSource", "toString", "toLocaleString", "join", "reverse", "sort", "push", "pop", "shift", "unshift", "splice", "concat", "slice", "lastIndexOf", "indexOf", "forEach", "map", "filter", "reduce", "reduceRight", "some", "every", "find", "findIndex", "copyWithin", "fill", "entries", "keys", "values", "includes", "constructor", "$set", "$remove"]
3     console.log(Object.getOwnPropertyNames(Object.prototype))
4 // ["toSource", "toString", "toLocaleString", "valueOf", "watch", "unwatch", "hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable", "__defineGetter__", "__defineSetter__", "__lookupGetter__", "__lookupSetter__", "__proto__", "constructor"]

  我相信,大家看到这里,对prototype还是似懂非懂的。这很正常,毕竟是js中比较重要又比较抽象的概念,不可能那么快就掌握,再啃多几篇,说不定掌握其精髓。在某乎上,有一个活生生的实例,可能也是大家会遇到的问题。可以看看 js构造函数和原型对象

5.constructor属性

  prototype对象有一个constructor属性,默认指向prototype对象所在的构造函数。

1     function A(){};
2     console.log(A.prototype.constructor===A)    //true

  要注意的是,prototype是构造函数的属性,而constructor则是构造函数的prototype属性所指向的那个对象,也就是原型对象的属性。注意不要混淆。

1     console.log(A.hasOwnProperty(‘prototype‘));    //true
2     console.log(A.prototype.hasOwnProperty(‘constructor‘)); //true

  由于constructor属性是定义在原型(prototype)对象上面,意味着可以被所有实例对象继承。

1     function A(){};
2     var a=new A();
3
4     console.log(a.constructor);    //A()
5     console.log(a.constructor===A.prototype.constructor);//true

  上面代码中,a是构造函数A的实例对象,但是a自身没有contructor属性,该属性其实是读取原型链上面的A.prototype.constructor属性。

  5.1:constructor属性的作用

    a:分辨原型对象到底属于哪个构造函数

1     function A(){};
2     var a=new A();
3
4     console.log(a.constructor===A)    //true
5     console.log(a.constructor===Array)    //false

    上面代码表示,使用constructor属性,确定实例对象a的构造函数是A,而不是Array

    b:从实例新建另一个实例

1     function A() {};
2     var a = new A();
3     var b = new a.constructor();
4     console.log(b instanceof A);    //true

    上面代码中,a是构造函数A的实例对象,可以从a.constructor间接调用构造函数。

    c:调用自身的构造函数成为可能

1     A.prototype.hello = function() {
2         return new this.constructor();
3     }

    d:提供了一种从构造函数继承另外一种构造函数的模式

1     function Father() {}
2
3     function Son() {
4         Son.height.constructor.call(this);
5     }
6
7     Son.height = new Father();

    上面代码中,FatherSon都是构造函数,在Son内部的this上调用Father,就会形成Son继承Father的效果。

    e:由于constructor属性是一种原型对象和构造函数的关系,所以在修改原型对象的时候,一定要注意constructor的指向问题。

    解决方法有两种,要么将constructor属性指向原来的构造函数,要么只在原型对象上添加属性和方法,避免instanceof失真。

6.instanceof运算符

  instanceof运算符返回一个布尔值,表示指定对象是否为某个构造函数的实例。

1     function A() {};
2     var a = new A();
3     console.log(a instanceof A);    //true

  因为instanceof对整个原型链上的对象都有效,所以同一个实例对象,可能会对多个构造函数都返回true。

1     function A() {};
2     var a = new A();
3     console.log(a instanceof A);    //true
4     console.log(a instanceof Object);    //true

  注意,instanceof对象只能用于复杂数据类型(数组,对象等),不能用于简单数据类型(布尔值,数字,字符串等)。

1     var x = [1];
2     var o = {};
3     var b = true;
4     var c = ‘string‘;
5     console.log(x instanceof Array);    //true
6     console.log(o instanceof Object);    //true
7     console.log(b instanceof Boolean);    //false
8     console.log(c instanceof String);    //false

  此外,null和undefined都不是对象,所以instanceof 总是返回false。

1     console.log(null instanceof Object);    //false
2     console.log(undefined instanceof Object);    //false

  利用instanceof运算符,还可以巧妙地解决,调用构造函数时,忘了加new命令的问题。

1     function Keith(name,height) {
2         if (! this instanceof Keith) {
3             return new Keith(name,height);
4         }
5         this.name = name;
6         this.height = height;
7     }

  上面代码中,使用了instanceof运算符来判断函数体内的this关键字是否指向构造函数Keith的实例,如果不是,就表明忘记加new命令,此时构造函数会返回一个对象实例,避免出现意想不到的结果。

   因为限于篇幅的原因,暂时介绍到这里。

  我会在下次的分享中谈谈原型(prototype)对象的一些原生方法,如Object.getPrototypeOf(),Object.setPrototypeOf()等,并且介绍获取原生对象方法的比较。

完。

感谢大家的阅读。

时间: 2024-10-19 18:08:40

Javascript中prototype属性的详解的相关文章

javascript中=、==、===区别详解

javascript中=.==.===区别详解今天在项目开发过中发现在一个小问题.在判断n==""结果当n=0时 n==""结果也返回了true.虽然是个小问题,却有着大影响,所以决定深入分析下.1.= 赋值运算符 //例: var n=1; console.log(n);//1 n=n+1; console.log(n);//2 2.== 值比较运算符 值比较运算符在表达式两边的数据类型不一致时,会隐式转换为相同数据类型,然后对值进行比较. //例: var a=

Javascript中prototype属性

prototype作为JS相对比较难理解的一个知识点,在这里发表下自己的理解. 本文将包含以下几部分内容: 1.js prototype的简单介绍, 2.js构造函数的介绍, 3.prototype的深入理解, 4.constructor. 一.在其他的面向对象语音中,比如Java,存在类(class)的概念,对象就是类的实例.但是再js当中呢,是没有类的概念的,平常时说的加一个class类是指在样式css中加一个类class.js中一切皆对象,所有的东西都是对象(除了null和undefine

JavaScript中的Date类型详解与moment简介

关于JavaScript中的Date类型,相信JSer们都不会陌生吧,但是也必然为那个复杂难记的各种转换函数所头疼,本文将分享一下我对JS中的Date类型的一些知识小总结,并把其中容易犯错的地方指出来,同时简介和推广moment.js这个js库,希望大家看完文章后以后在对Date类处理如鱼得水. 1 时间的唯一性与多样性 某一时刻在全世界任何地区应该是唯一的,时区的不同是为了让地球不同时区的人的中午十二点都是太阳正上当头,形成交流上没有那么多障碍.而这一标准就是大家熟知的格林威治标准时间(Gre

javascript 中的 this 关键字详解

1.javascript 中 什么是 this? this 指的是当前行为执行的主体,或者是当前方法执行的主体 context:是当前行为或者方法执行的环境 实例: xx 去北京饭店吃东西:上下文是“北京饭店”, this 是 xx 2. 那么如何判断一个函数在执行的时候,函数体内的 this 关键字是谁呢?主要有以下几条规律:  1)一个函数体内的 this 关键字和这个函数的在哪里定义,哪里执行都没有关系;   2) 判断一个方法执行的时候,函数的执行主体是谁?主要看方法前面有没有点(.),

Javascript中的原型继承详解

js中的继承,是面向对象的知识,因为js没有类的概念,所以继承是通过对象实现的,谈到继承,就必须说到prototype,就不得不先说下new的过程. 一个小小的列子: <script type="text/javascript"> var Person = function () { }; var p = new Person(); </script> 我们来看看这个new究竟做了什么?我们可以把new的过程拆分成以下三步: <1> var p={}

JavaScript中数组Array方法详解

ECMAScript 3在Array.prototype中定义了一些很有用的操作数组的函数,这意味着这些函数作为任何数组的方法都是可用的. 1.Array.join()方法 Array.join()方法将数组中所有元素都转化为字符串并连接在一起,返回最后生成的字符串.可以指定一个可选的符号或字符串在生成的字符串中来分隔数组的各个元素.如果不指定分隔符,默认使用逗号.注意:此方法不会改变原始数组 var arr = ['a', 'b', 'c']; console.log(arr.join());

JavaScript中的Array数组详解

ECMAScript中的数组与其他多数语言中的数组有着相当大的区别,虽然数组都是数据的有序列表,但是与其他语言不同的是,ECMAScript数组的每一项可以保存任何类型的数据.也就是说,可以用数组的第一个位置来保存字符串,第二个位置保存数值,第三个位置保存对象,而且ECMAScript数组的大小是可以动态调整的,即可以随着数据的添加自动增长以容纳新增数据. 数组的创建 创建数组的基本方式有两种,第一种是使用Array构造函数. var arr = new Array(); 如果预先知道数组要保存

JavaScript中的跨域详解(内附源码)

什么是跨域? 什么是跨域? 所谓跨域,就是如果在不同的域名下面存在数据交互,这个时候就会存在跨域的问题,这里要说明的是在同一个网站不同的文件夹下的数据交互是不存在跨域问题的. 哪些情况下存在跨域问题? 主域和子域之间会存在跨域问题(比如 www.a.com 和 www.a.b.com). 不同的域名存在跨域问题,哪怕这两个域名指向的是同一个ip地址. 为什么 ajax 不可以跨域? 因为 ajax 是通过 XMLHttpRequest 这个对象来进行数据之间的交互的,而 XMLHttpReque

Android中PropertyAnimation属性动画详解(一)

在之前的文章中已经讲了帧动画frame-by-frame animation和补间动画tweened animation,其实这两种动画原理好简单,都是按照预先固定的动画模式来播放的,帧动画将一张张单独的图片,然后把它们连贯起来进行播放,就形成了动画效果,补间动画则是可以对View对象进行一系列的动画操作,包括淡入淡出.缩放.平移.旋转四种,不过这几种都是完全按照我们预先设置好的效果来执行,不能动态的改变,所以,在Android3.0以后就引入了属性动画PropertyAnimation来满足一