首页 web前端 js教程 浅谈JavaScript 对象创建模式

浅谈JavaScript 对象创建模式

Jan 23, 2017 pm 02:50 PM

简介

随着基于web的应用程序不断普及以及一些插件的消逝(Flash,Siverlight,Java Applets, ...),越来越多的开发者发现他们正在使用JavaScript编写复杂应用程序.

很多开发者情愿借助一些具有继承功能的第三方框架诸如prototype.js之类的,以便可以按以前熟悉的方式编程,而不用自己去实现一个继承。我也一样。然而,很快我就因为对它的原理知之甚少而感到愧疚。

我起初本想写一篇关于javascript继承模式的文章,不久之后我发现即便是一个很简单的对象创建也是有很多地方可以深入研究的。

我知道你可能会说,创建一个javascript的对象?这再容易不过了,像这样子:

var myObject = {};
登录后复制

普通的对象

JavaScript 是一门很简单的语言,只有几种内置的类型。在这一节中我们会详细研究对象类型,它是怎么创建的和它继承的属性。JavaScript 里对象只是属性的集合。属性由(唯一的)名和任意类型的值组成

对象是动态的,对象被创建以后,你可以增加和删除它的属性。下面的代码创建了一个“空”的对象,然后加入了几个属性:

var shape = {};
shape.color = 'red';
shape.borderThickness = '2.0';
登录后复制

你也可以像下面这样子更简单地实现对象的初始化:

var shape = { 
    color : 'red',
    borderThickness : '2.0'
}
登录后复制

上面两种形式得到的对象是相同的。

我们还有第三种方式,是用Object的构造函数:

var shape = new Object();

这也是与上面其他两种方式等价的,然而我们忽略了一个问题!

我们所创建的这个对象到底长什么样子?以及它有什么属性?可以通过谷歌浏览器(或者IE、Safari、firefox等等)的开发者工具我们可以很容易地审查它。通常我们可以通过添加监视或者简单地在控制台输入它的名字来审查一个对象。

这里就是那个shape的对象:

145105_wDq6.png

正如你所看到的它有两个属性,color和borderThickness,这是被显式地添加的。但是这个神秘的__proto__又是什么东西呢?

从它是用的下横线你或许会猜到它是一个私有属性。然而,众所周知javascript只有公有属性,谁也不能阻止你在代码里使用它们(即使有危险)。

__proto__ 这个属性指向了对象的prototype,打开这个对象之后,你会发现他有不少的函数。

145107_Abio.png

这揭露了JavaScript 真正的继承机制是围绕着原型链的。如果你试着访问一个不在对象中的属性(无论该属性是一个值还是一个函数),JavaScript 运行时就会检查这个对象的原型是否有这个属性。如果有,它就返回这个属性,否则会顺着原型链向上一直查找。

你可以试一下。在控制台中试着访问属性 toString (你可以用点方法或者索引方法),这个属性是一个函数,你可以调用它。

145108_blSt.png

虽然结果并不是十分有趣!

你可以看见,JavaScript 的对象十分简单,就是一个“袋子”,装着属性和允许属性继承的“隐藏的”原型。

JavaScript 与生俱来的动态性让创建对象变得十分简单,然而,大部分复杂的应用都是利用类型(在C#和Java语言中定义为类)。类型描述了对象的属性和方法,允许你创建多个类型的实例。

下一节中,我们会看看如何用JavaScript实现类型。

关于术语最后一点 - JavaScript 有函数,但是在对象属性上的函数被称为方法。

构造函数

上一节中我们简要地讲了原型链以及对象如何继承属性和方法。一旦对象被创建,你就不能更改它的原型。怎样给它添加属性和方法呢?

构造函数解决了上述问题,在深入之前,我们先给对象添加一个函数,使例子变得更贴合实际:

var shape = { 
    color : 'red',
    borderThickness : '2.0',
    describe : function() {       
       console.log("I am a " + this.color + " shape, with a border that is " +           
          this.borderThickness + " thick");
    }
}
登录后复制

这给对象shape加了一个函数(也就是方法)。如果你用控制台审查,你会看见describe这个函数是一个属性,和其他东西一样。你可以通过控制台调用这个方法:

145109_PskG.png

构造函数执行完成会在创建的“shape”对象上添加两个属性,同对象初始化创建的“shape”对象相比,你会发现两者看起来似乎一样。然而有个微妙的区别,初始化创建的“shape”对象有个"隐藏”属性__proto__,Chrome指定为Object,而构造函数创建的“shape”对象的__proto__指向Shape。(注:随后你会明白,Chrome使用的这种指定方式!)

译者注:不知道原文作者的Chrome版本,我的机子上Chrome版本是 53.0.2785.113 m (64-bit)。通过构造器构造的对象的__proto__还是指向一个Object,即Shape函数的prototype属性,理论上也应该是Object。后文翻译还是以原作者的Chrome为准:构造函数实例的__proto__为Shape,初始化对象的__proto__为Object。

在讨论这两者微妙的区别的意图之前,我们来为shape对象添加一个describe方法。

你可以在刚才创建的对象的构造函数上直接添加方法。然而,这种构造模式(译者注:指构造函数创建对象的方式)提供了一种更为有力的替代方式。你可以在构造函数的原型属性上添加方法。如下所示:

function Shape(color, borderThickness) { 
    this.color = color;    
    this.borderThickness = borderThickness;
}
Shape.prototype.describe = function() {    
      console.log("I am a " + this.color + " shape, with a border that is " +        
      this.borderThickness + " thick");
};
登录后复制

若你创建一个shape对象并审查此对象,你会发现如下所示代码:

145112_DOHk.png

现在你的shape对象具有describe方法,但这并非对象本身的属性,而是该对象原型上的一个属性。通过上文,我们明白一个定义在Object原型上的方法是如何通过原型链被实例继承的。同样,所有的Shape实例会继承describe方法:

145112_2LBI.png

展开Shape的__proto__属性所指的对象,会显示此对象位于原型链的底部:

145116_BmEb.png

因此,通过Shape构造器创建的任意对象都可以访问上述显示的所有方法。

JavaScript中所谓的类,其实是一种设计模式:一个构造函数(consturctor)和一个用于在该类实例间共享属性和方法的原型对象(Objcet.prototype)的结合。

为了达到属性继承,代码复用等目的,通过函数来模拟类来创建对象。

(或许你会疑惑,通过Object就可以创建对象了呀?确实如此,new一个Object对象后,给Object对象增加属性和方法,确实可以生成一个对象。但这种做法实在太麻烦,且不易封装复用。下面介绍的方法一就是这种方法的高级版)

方法一:用一个普通的函数来封装(俗称工厂模式)

function createPerson(name, age){
    var o = new Object();
    o.name = name;
    o.age = age;
    o.sayName = function(){
        alert(this.name);
    };    
    return o;
}
var p1 = createPerson("Jack", 32);
var p2 = createPerson("Zhang", 30);
p1.sayName();   //Jack
p2.sayName();   //Zhang
登录后复制

这个例子平淡无奇,createPerson函数内创建了一个Object,并赋予它两个属性(name,age)和一个方法(sayName)。这样部分达到了类的作用。为何说是部分呢?因为它有个明显的缺陷,即无法解决对象识别问题,创建出来的对象(p1,p2)都是Object类型的:

alert(p1 instanceof Object);  //true  
alert(p2 instanceof Object);  //true
登录后复制

如果你有一堆对象,有Person有Dog,你无法区分这些对象中哪些是Person哪些是Dog。为了解决上述缺陷,引入了构造函数的概念

方法二:通过构造函数来封装:(俗称构造函数模式)

function Person(name, age){  
    this.name = name;  
    this.age = age;  
    this.sayName = function(){  
        alert(this.name);  
    };      
}  
  
var p1 = new Person("Jack", 32);    //用new操作符来调用  
var p2 = new Person("Zhang", 30);  
p1.sayName();   //Jack  
p2.sayName();   //Zhang  
  
alert(p1 instanceof Object);  //true,显然创建的对象都既是Object,也是Person  
alert(p1 instanceof Person);  //true  
alert(p2 instanceof Object);  //true  
alert(p2 instanceof Person);  //true  
  
alert(p1.constructor == Person);  //true  
alert(p1.constructor == Dog);     //false,这样就能区分对象究竟是Person还是Dog了
登录后复制

Person函数与createPerson函数相比,有以下不同:

1.没有var o = new Object();创建对象,自然最后也没有return o;返回对象

2.没有将属性和方法赋给Object对象,而是赋给this对象

因为Person函数内部使用了this对象,因此你必须用new操作符来创建对象:

var p = new Person("Jack", 32);
p.sayName();//Jack
登录后复制

如果你忘记用new操作符来创建对象的话:(解决方式见※1)

var p = Person("Jack", 32);  
p;              //undefined  
this.name;          //Jack  
this.age;           //32
登录后复制

灾难的事情发生了,因为没有用new来调用,因此Person函数内的this将指向window对象(即BOM),因此等于给window对象添加了两个全局变量。

像Person这样的函数被称为【构造函数】。你可能疑惑,既然JavaScript并没有类的概念,那怎么会有构造函数呢?其实构造函数与普通函数唯一的区别,就在于它们的调用方式不同。只要通过new操作符来调用,那它就可以作为构造函数。但构造函数毕竟也是函数,并不存在任何特殊的语法。

但通过构造函数的方式来模拟类的话,也有个缺陷:(一种不好的解决方式见※2)

alert(p1.sayName == p2.sayName);  //false
登录后复制

sayName只是个普通的方法,如果你创建多少个Person对象,显然你不希望多个对象中都有一个sayName方法的副本。这将造成无谓的浪费。

方法三:通过原型来封装:(俗称原型模式,但其实这里的例子是复合模式,即构造函数模式+原型模式)

每个函数都有个prototype属性,该属性其实是一个指针,指向一个对象,称为原型对象。原型对象中包含着可供所有实例共享的属性和方法。

可以将希望所有对象共享的属性或方法(如上述sayName方法)添加到原型对象中,达到继承复用的目的。

function Person(name, age) {  
    this.name = name;  //name和age没有放到原型对象中,而是仍旧留在构造函数内部  
    this.age = age;    //表示不希望每个实例都共享这两个数  
}  
Person.prototype.sayName = function(){    //将希望被所有对象共享的sayName方法放入原型对象中  
    alert(this.name);  
}  
  
var p1 = new Person("Jack", 32);    //用new操作符来调用  
var p2 = new Person("Zhang", 30);  
p1.sayName();   //Jack  
p2.sayName();   //Zhang  
<pre name="code" class="javascript">alert(p1.sayName == p2.sayName);  //true,sayName方法确实被共享了,而不是每个对象中都有一个独立的副本
登录后复制

示意图:

20150325105142179.jpg

Person构造函数(再次声明,无论什么函数内部都有原型指针,指向一个原型对象)内部有个原型指针,指向Person的原型对象。图中也可看出Person的原型对象内部有个constructor属性,指向对应的函数。这样函数和原型对象就双向绑定起来了。

已经展示了忘记用new操作符来调用构造函数的可怕后果。靠程序员自觉显然不是个好主意。有3种方式可以避免这种错误:

1.在函数内部第一行加上use strict;启用严格模式,这样var p = Person("Jack", 32);创建将失败。缺点是你必须保证环境支持ES5,否则无法保证严格模式能起作用。

2.改进构造函数:

function Person(name, age) {   
    if(!(this instanceof Person)){  
        return new Person(name, age);  
    }  
    this.name = name;  
    this.age= age;   
}
登录后复制

上述代码是自解释代码(self-explanatory),一行注释都不用就能看明白。缺点是需要额外的开销(即额外的函数调用),也无法适用于可变参数函数。

3.利用ES5的Object.create函数改进:

function Person(name, age) {  
        var self = this instanceof Person? this : Object.create(Person.prototype);  
        self.name = name;  
        self.age= age;  
        return self;  
}
登录后复制

Object.create需要一个原型对象作为参数,并返回一个继承自该原型对象的新对象。同样你必须保证环境支持ES5

已经展示了通过构造函数来模拟类的缺陷,即无法实现共享。你可能会疑惑,通过共通函数不就能实现共享了吗?

function Person(name, age) {  
        this.name = name;  
        this.age= age;  
        this.sayName = sayName;  
}  
  
function sayName() {  
        alert(this.name);  
}  
  
var p1 = new Person("Jack", 32);  
var p2 = new Person("Zhang", 30);  
  
alert(p1.sayName == p2.sayName);  //true
登录后复制

在构造函数中定义个函数指针,指向全局函数sayName,这样确实能达到共享的目的。但你真希望看到全局函数吗?如果Person有5,6个方法,那你就需要定义5,6个全局函数,同样是个灾难。

总结

在JavaScript中,对象创建并不是一个简单的主题!希望通读此文后,你会学到些关于构造函数,原型或JavaScript语言的其它内容。


本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

AI Hentai Generator

AI Hentai Generator

免费生成ai无尽的。

热门文章

R.E.P.O.能量晶体解释及其做什么(黄色晶体)
3 周前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳图形设置
3 周前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您听不到任何人,如何修复音频
3 周前 By 尊渡假赌尊渡假赌尊渡假赌

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

在JavaScript中替换字符串字符 在JavaScript中替换字符串字符 Mar 11, 2025 am 12:07 AM

JavaScript字符串替换方法详解及常见问题解答 本文将探讨两种在JavaScript中替换字符串字符的方法:在JavaScript代码内部替换和在网页HTML内部替换。 在JavaScript代码内部替换字符串 最直接的方法是使用replace()方法: str = str.replace("find","replace"); 该方法仅替换第一个匹配项。要替换所有匹配项,需使用正则表达式并添加全局标志g: str = str.replace(/fi

构建您自己的Ajax Web应用程序 构建您自己的Ajax Web应用程序 Mar 09, 2025 am 12:11 AM

因此,在这里,您准备好了解所有称为Ajax的东西。但是,到底是什么? AJAX一词是指用于创建动态,交互式Web内容的一系列宽松的技术。 Ajax一词,最初由Jesse J创造

如何创建和发布自己的JavaScript库? 如何创建和发布自己的JavaScript库? Mar 18, 2025 pm 03:12 PM

文章讨论了创建,发布和维护JavaScript库,专注于计划,开发,测试,文档和促销策略。

如何在浏览器中优化JavaScript代码以进行性能? 如何在浏览器中优化JavaScript代码以进行性能? Mar 18, 2025 pm 03:14 PM

本文讨论了在浏览器中优化JavaScript性能的策略,重点是减少执行时间并最大程度地减少对页面负载速度的影响。

如何使用浏览器开发人员工具有效调试JavaScript代码? 如何使用浏览器开发人员工具有效调试JavaScript代码? Mar 18, 2025 pm 03:16 PM

本文讨论了使用浏览器开发人员工具的有效JavaScript调试,专注于设置断点,使用控制台和分析性能。

jQuery矩阵效果 jQuery矩阵效果 Mar 10, 2025 am 12:52 AM

将矩阵电影特效带入你的网页!这是一个基于著名电影《黑客帝国》的酷炫jQuery插件。该插件模拟了电影中经典的绿色字符特效,只需选择一张图片,插件就会将其转换为充满数字字符的矩阵风格画面。快来试试吧,非常有趣! 工作原理 插件将图片加载到画布上,读取像素和颜色值: data = ctx.getImageData(x, y, settings.grainSize, settings.grainSize).data 插件巧妙地读取图片的矩形区域,并利用jQuery计算每个区域的平均颜色。然后,使用

如何构建简单的jQuery滑块 如何构建简单的jQuery滑块 Mar 11, 2025 am 12:19 AM

本文将引导您使用jQuery库创建一个简单的图片轮播。我们将使用bxSlider库,它基于jQuery构建,并提供许多配置选项来设置轮播。 如今,图片轮播已成为网站必备功能——一图胜千言! 决定使用图片轮播后,下一个问题是如何创建它。首先,您需要收集高质量、高分辨率的图片。 接下来,您需要使用HTML和一些JavaScript代码来创建图片轮播。网络上有很多库可以帮助您以不同的方式创建轮播。我们将使用开源的bxSlider库。 bxSlider库支持响应式设计,因此使用此库构建的轮播可以适应任何

如何使用Angular上传和下载CSV文件 如何使用Angular上传和下载CSV文件 Mar 10, 2025 am 01:01 AM

数据集对于构建API模型和各种业务流程至关重要。这就是为什么导入和导出CSV是经常需要的功能。在本教程中,您将学习如何在Angular中下载和导入CSV文件

See all articles