javascript面向对象系列第一篇——构造函数和原型对象

时间:2021-07-16 21:39:07

前面的话

  一般地,javascript使用构造函数和原型对象来进行面向对象编程,它们的表现与其他面向对象编程语言中的类相似又不同。本文将详细介绍如何用构造函数和原型对象来创建对象

构造函数

  构造函数是用new创建对象时调用的函数,与普通唯一的区别是构造函数名应该首字母大写

function Person(){
this.age = 30;
}
var person1 = new Person();
console.log(person1.age);//

  根据需要,构造函数可以接受参数

function Person(age){
this.age = age;
}
var person1 = new Person(30);
console.log(person1.age);//

  如果没有参数,可以省略括号

function Person(){
this.age = 30;
}
//等价于var person1 = new Person()
var person1 = new Person;
console.log(person1.age);//30

  如果忘记使用new操作符,则this将代表全局对象window

function Person(){
this.age = 30;
}
var person1 = Person();
//Uncaught TypeError: Cannot read property 'age' of undefined
console.log(person1.age);

instanceof

  instanceof操作符可以用来鉴别对象的类型

function Person(){
//
}
var person1 = new Person;
console.log(person1 instanceof Person);//true

constructor

  每个对象在创建时都自动拥有一个构造函数属性constructor,其中包含了一个指向其构造函数的引用。而这个constructor属性实际上继承自原型对象,而constructor也是原型对象唯一的自有属性

function Person(){
//
}
var person1 = new Person;
console.log(person1.constructor === Person);//true
console.log(person1.__proto__.constructor === Person);//true

  以下是person1的内部属性,发现constructor是继承属性

javascript面向对象系列第一篇——构造函数和原型对象

  虽然对象实例及其构造函数之间存在这样的关系,但是还是建议使用instanceof来检查对象类型。这是因为构造函数属性可以被覆盖,并不一定完全准确

function Person(){
//
}
var person1 = new Person;
Person.prototype.constructor = 123;
console.log(person1.constructor);//
console.log(person1.__proto__.constructor);//

返回值

  函数中的return语句用来返回函数调用后的返回值,而new构造函数的返回值有点特殊

  如果构造函数使用return语句但没有指定返回值,或者返回一个原始值,那么这时将忽略返回值,同时使用这个新对象作为调用结果

function fn(){
this.a = 2;
return;
}
var test = new fn();
console.log(test);//{a:2}

  如果构造函数显式地使用return语句返回一个对象,那么调用表达式的值就是这个对象

var obj = {a:1};
function fn(){
this.a = 2;
return obj;
}
var test = new fn();
console.log(test);//{a:1}

  所以,针对丢失new的构造函数的解决办法是在构造函数内部使用instanceof判断是否使用new命令,如果发现没有使用,则直接使用return语句返回一个实例对象

function Person(){
if(!(this instanceof Person)){
return new Person();
}
this.age = 30;
}
var person1 = Person();
console.log(person1.age);//
var person2 = new Person();
console.log(person2.age);//

  使用构造函数的好处在于所有用同一个构造函数创建的对象都具有同样的属性和方法

function Person(name){
this.name = name;
this.sayName = function(){
console.log(this.name);
}
}
var person1 = new Person('bai');
var person2 = new Person('hu');
person1.sayName();//'bai'

  构造函数允许给对象配置同样的属性,但是构造函数并没有消除代码冗余。使用构造函数的主要问题是每个方法都要在每个实例上重新创建一遍。在上面的例子中,每一个对象都有自己的sayName()方法。这意味着如果有100个对象实例,就有100个函数做相同的事情,只是使用的数据不同

function Person(name){
this.name = name;
this.sayName = function(){
console.log(this.name);
}
}
var person1 = new Person('bai');
var person2 = new Person('hu');
console.log(person1.sayName === person2.sayName);//false

  可以通过把函数定义转换到构造函数外部来解决问题

function Person(name){
this.name = name;
this.sayName = sayName;
}
function sayName(){
console.log(this.name);
}
var person1 = new Person('bai');
var person2 = new Person('hu');
console.log(person1.sayName === person2.sayName);//true

  但是,在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。而且,如果对象需要定义很多方法,就要定义很多全局函数,严重污染全局空间,这个自定义的引用类型没有封装性可言了

  如果所有的对象实例共享同一个方法会更有效率,这就需要用到下面所说的原型对象

原型对象

  说起原型对象,就要说到原型对象、实例对象和构造函数的三角关系

  接下来以下面两行代码,来详细说明它们的关系

function Foo(){};
var f1 = new Foo;

构造函数

  用来初始化新创建的对象的函数是构造函数。在例子中,Foo()函数是构造函数

实例对象

  通过构造函数的new操作创建的对象是实例对象,又常常被称为对象实例。可以用一个构造函数,构造多个实例对象。下面的f1和f2就是实例对象

function Foo(){};
var f1 = new Foo;
var f2 = new Foo;
console.log(f1 === f2);//false

原型对象及prototype

  通过构造函数的new操作创建实例对象后,会自动为构造函数创建prototype属性,该属性指向实例对象的原型对象。通过同一个构造函数实例化的多个对象具有相同的原型对象。下面的例子中,Foo.prototype是原型对象

function Foo(){};
Foo.prototype.a = 1;
var f1 = new Foo;
var f2 = new Foo; console.log(Foo.prototype.a);//
console.log(f1.a);//
console.log(f2.a);//

constructor

  原型对象默认只会取得一个constructor属性,指向该原型对象对应的构造函数。至于其他方法,则是从Object继承来的

function Foo(){};
console.log(Foo.prototype.constructor === Foo);//true
javascript面向对象系列第一篇——构造函数和原型对象
  由于实例对象可以继承原型对象的属性,所以实例对象也拥有constructor属性,同样指向原型对象对应的构造函数
function Foo(){};
var f1 = new Foo;
console.log(f1.constructor === Foo);//true
javascript面向对象系列第一篇——构造函数和原型对象
proto
  实例对象内部包含一个proto属性(IE10-浏览器不支持该属性),指向该实例对象对应的原型对象
function Foo(){};
var f1 = new Foo;
console.log(f1.__proto__ === Foo.prototype);//true

  [注意]关于proto、constructor和prototype这三者的详细图例关系移步至此

isPrototypeOf()

  一般地,可以通过isPrototypeOf()方法来确定对象之间是否是实例对象和原型对象的关系 

function Foo(){};
var f1 = new Foo;
console.log(f1.__proto__ === Foo.prototype);//true
console.log(Foo.prototype.isPrototypeOf(f1));//true

Object.getPrototypeOf()

  ES5新增了Object.getPrototypeOf()方法,该方法返回实例对象对应的原型对象 

function Foo(){};
var f1 = new Foo;
console.log(Object.getPrototypeOf(f1) === Foo.prototype);//true

  实际上,Object.getPrototypeOf()方法和__proto__属性是一回事,都指向原型对象

function Foo(){};
var f1 = new Foo;
console.log(Object.getPrototypeOf(f1) === f1.__proto__ );//true

属性查找

  当读取一个对象的属性时,javascript引擎首先在该对象的自有属性中查找属性名字。如果找到则返回。如果自有属性不包含该名字,则javascript会搜索proto中的对象。如果找到则返回。如果找不到,则返回undefined

var o = {};
console.log(o.toString());//'[object Object]' o.toString = function(){
return 'o';
}
console.log(o.toString());//'o' delete o.toString;
console.log(o.toString());//'[objet Object]'

in

  in操作符可以判断属性在不在该对象上,但无法区别自有还是继承属性

var o = {a:1};
var obj = Object.create(o);
obj.b = 2;
console.log('a' in obj);//true
console.log('b' in obj);//true
console.log('b' in o);//false
//Object.create()是创建对象的一种方法,等价于
function Test(){};
var obj = new Test;
Test.prototype.a = 1;
obj.b = 2;
console.log('a' in obj);//true
console.log('b' in obj);//true
console.log('b' in Test.prototype);//false

hasOwnProperty()

  通过hasOwnProperty()方法可以确定该属性是自有属性还是继承属性

var o = {a:1};
var obj = Object.create(o);
obj.b = 2;
console.log(obj.hasOwnProperty('a'));//false
console.log(obj.hasOwnProperty('b'));//true

  于是可以将hasOwnProperty方法和in运算符结合起来使用,用来鉴别原型属性

function hasPrototypeProperty(object,name){
return name in object && !object.hasOwnProperty(name);
}

  原型对象的共享机制使得它们成为一次性为所有对象定义方法的理想手段。因为一个方法对所有的对象实例做相同的事,没理由每个实例都要有一份自己的方法

function Person(name){
this.name = name;
}
Person.prototype.sayName = function(){
console.log(this.name);
}
var person1 = new Person('bai');
var person2 = new Person('hu'); person1.sayName();//'bai'

  可以在原型对象上存储其他类型的数据,但在存储引用值时需要注意。因为这些引用值会被多个实例共享,一个实例能够改变另一个实例的值

function Person(name){
this.name = name;
}
Person.prototype.sayName = function(){
console.log(this.name);
}
Person.prototype.favoraties = []; var person1 = new Person('bai');
var person2 = new Person('hu'); person1.favoraties.push('pizza');
person2.favoraties.push('quinoa');
console.log(person1.favoraties);//["pizza", "quinoa"]
console.log(person2.favoraties);//["pizza", "quinoa"]

  虽然可以在原型对象上一一添加属性,但是直接用一个对象字面形式替换原型对象更简洁

function Person(name){
this.name = name;
}
Person.prototype = {
sayName: function(){
console.log(this.name);
},
toString : function(){
return '[person ' + this.name + ']'
}
}; var person1 = new Person('bai'); console.log(person1 instanceof Person);//true
console.log(person1.constructor === Person);//false
console.log(person1.constructor === Object);//true

  当一个函数被创建时,该原型对象的constructor属性自动创建,并指向该函数。当使用对象字面形式改写原型对象Person.prototype时,需要在改写原型对象时手动重置其constructor属性

function Person(name){
this.name = name;
}
Person.prototype = {
constructor: Person,
sayName: function(){
console.log(this.name);
},
toString : function(){
return '[person ' + this.name + ']'
}
}; var person1 = new Person('bai'); console.log(person1 instanceof Person);//true
console.log(person1.constructor === Person);//true
console.log(person1.constructor === Object);//false

  由于默认情况下,原生的constructor属性是不可枚举的,更妥善的解决方法是使用Object.defineProperty()方法,改变其属性描述符中的枚举性enumerable

function Person(name){
this.name = name;
}
Person.prototype = {
sayName: function(){
console.log(this.name);
},
toString : function(){
return '[person ' + this.name + ']'
}
};
Object.defineProperty(Person.prototype,'constructor',{
enumerable: false,
value: Person
});
var person1 = new Person('bai');
console.log(person1 instanceof Person);//true
console.log(person1.constructor === Person);//true
console.log(person1.constructor === Object);//false

总结

  构造函数、原型对象和实例对象之间的关系是实例对象和构造函数之间没有直接联系

function Foo(){};
var f1 = new Foo;

  以上代码的原型对象是Foo.prototype,实例对象是f1,构造函数是Foo

  原型对象和实例对象的关系

console.log(Foo.prototype === f1.__proto__);//true

  原型对象和构造函数的关系

console.log(Foo.prototype.constructor === Foo);//true

  而实例对象和构造函数则没有直接关系,间接关系是实例对象可以继承原型对象的constructor属性

console.log(f1.constructor === Foo);//true

  如果非要扯实例对象和构造函数的关系,那只能是下面这句代码,实例对象是构造函数的new操作的结果

var f1 = new Foo;

  这句代码执行以后,如果重置原型对象,则会打破它们三个的关系

function Foo(){};
var f1 = new Foo;
console.log(Foo.prototype === f1.__proto__);//true
console.log(Foo.prototype.constructor === Foo);//true Foo.prototype = {};
console.log(Foo.prototype === f1.__proto__);//false
console.log(Foo.prototype.constructor === Foo);//false

  所以,代码顺序很重要

参考资料

【1】 阮一峰Javascript标准参考教程——面向对象编程概述 http://javascript.ruanyifeng.com/oop/basic.html
【2】《javascript权威指南(第6版)》第6章 对象
【3】《javascript高级程序设计(第3版)》第6章 面向对象的程序设计
【4】《javascript面向对象精要》 第4章 构造函数和原型对象
【5】《你不知道的javascript上卷》第5章 原型

javascript面向对象系列第一篇——构造函数和原型对象的更多相关文章

  1. 深入理解javascript函数系列第一篇——函数概述

    × 目录 [1]定义 [2]返回值 [3]调用 前面的话 函数对任何一门语言来说都是一个核心的概念.通过函数可以封装任意多条语句,而且可以在任何地方.任何时候调用执行.在javascript里,函数即 ...

  2. 深入理解javascript函数系列第一篇

    前面的话 函数对任何一门语言来说都是核心的概念.通过函数可以封装任意多条语句,而且可以在任何地方.任何时候调用执行.在javascript里,函数即对象,程序可以随意操控它们.函数可以嵌套在其他函数中 ...

  3. javascript面向对象系列第二篇——创建对象的5种模式

    × 目录 [1]字面量 [2]工厂模式 [3]构造函数[4]原型模式[5]组合模式 前面的话 如何创建对象,或者说如何更优雅的创建对象,一直是一个津津乐道的话题.本文将从最简单的创建对象的方式入手,逐 ...

  4. 深入理解javascript作用域系列第一篇——内部原理

    × 目录 [1]编译 [2]执行 [3]查询[4]嵌套[5]异常[6]原理 前面的话 javascript拥有一套设计良好的规则来存储变量,并且之后可以方便地找到这些变量,这套规则被称为作用域.作用域 ...

  5. 深入理解javascript作用域系列第一篇

    前面的话 javascript拥有一套设计良好的规则来存储变量,并且之后可以方便地找到这些变量,这套规则被称为作用域.作用域貌似简单,实则复杂,由于作用域与this机制非常容易混淆,使得理解作用域的原 ...

  6. javascript动画系列第一篇——模拟拖拽

    × 目录 [1]原理介绍 [2]代码实现 [3]代码优化[4]拖拽冲突[5]IE兼容 前面的话 从本文开始,介绍javascript动画系列.javascript本身是具有原生拖放功能的,但是由于兼容 ...

  7. 前端学PHP之面向对象系列第一篇——类和对象

    × 目录 [1]类 [2]成员属性[3]成员方法[4]对象[5]成员访问[6]this 前面的话 面向对象程序设计(OOP)是一种计算机编程架构.计算机程序由单个能够起到子程序作用的单元或对象组成,为 ...

  8. javascript运动系列第一篇——匀速运动

    × 目录 [1]简单运动 [2]定时器管理 [3]分享到效果[4]移入移出[5]运动函数[6]透明度[7]多值[8]多物体[9]回调[10]函数完善[11]最终函数 前面的话 除了拖拽以外,运动也是j ...

  9. javascript面向对象系列第三篇——实现继承的3种形式

    × 目录 [1]原型继承 [2]伪类继承 [3]组合继承 前面的话 学习如何创建对象是理解面向对象编程的第一步,第二步是理解继承.本文是javascript面向对象系列第三篇——实现继承的3种形式 [ ...

随机推荐

  1. POJ 2785 HASH

    题目链接:http://poj.org/problem?id=2785 题意:给定n行数字,每行4个数分别是a,b,c,d,现在要求能有多少个(a,b,c,d)组合并且和为0 思路:n^2统计所有(a ...

  2. webForm中dropDownList的一些用法

    DropDownList 控件用于创建下拉列表. DropDownList 控件中的每个可选项都是由 ListItem 元素定义的! 该控件支持数据绑定! DropDownList1.DataSour ...

  3. JavaScript Tips

    Tips: return false - event.preventDefault(); //阻止默认行为 P.S 阻止a标签的跳转 - event.stopPropagation(); //阻止事件 ...

  4. SqlServer2008R2安装步骤

    参考http://jimshu.blog.51cto.com/3171847/585023/

  5. MFC 学习 之 工具栏的添加(一)

    最终实现的效果图: 步骤一:接下来在资源视图中添加一个ToolBar工具栏(具体怎么添加在这儿就不详细讲解了!)添加后的ToolBar以及工具栏中每个按钮 所命名的ID如下:(可以自定义,只要不重名就 ...

  6. Linux服务器集群系统(二)--转

    引用地址:http://www.linuxvirtualserver.org/zh/lvs2.html LVS集群的体系结构 章文嵩 (wensong@linux-vs.org) 2002 年 4 月 ...

  7. HDU-3487 Play with Chain Splay tee区间反转,移动

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=3487 对于一个数列有两种操作:1.CUT a b c,先取出a-b区间的数,然后把它们放在取出后的第c ...

  8. Asp.Net MVC 之 Autofac 初步使用2 集成mvc 属性注入以及自动注入

    首先看下Demo2的结构     然后下面一步步将Autofac集成到mvc中. 首先,定义Model Product.cs public class Product { public int Id ...

  9. Healwire Online Pharmacy 3.0 Cross Site Request Forgery / Cross Site Scripting

    Healwire Online Pharmacy version 3.0 suffers from cross site request forgery and cross site scriptin ...

  10. ASP.Net Core 2.2 MVC入门到基本使用系列 (一)(转)

    本教程会对基本的.Net Core 进行一个大概的且不会太深入的讲解, 在您看完本系列之后, 能基本甚至熟练的使用.Net Core进行Web开发, 感受到.Net Core的魅力. 本教程知识点大体 ...