深入JavaScript(一)——JavaScript中的面向对象(一)

面向对象的语言有一个标志,那就是它们都有“类”的概念,通过类可以创建任意多个具有相同属性和方法的对象。JavaScript 中没有类的概念,因此它的面向对象与基于类的语言中的对象有所不同。

JavaScript 对对象的定义是:无序属性的集合,其属性可以包含基本值、对象或者函数。可以把 JavaScript 对象理解成散列表,即一组名值对,其中的值可以是数据或函数。

那么想要创建自定义对象,有以下几种常用方法:

1.使用 Object 构造函数:

var person = new Object();
person.name = "Lucy";
person.sex = "female";
person.job = "nurse";
person.sayName = function() {
  alert(this.name);
};

2.使用对象字面量:

var person = {
  name: "Lucy",
  sex: "female",
  job: "nurse",
  sayName: function() {
    alert(this.name);
    }
};

上面这两种方式虽然都能用来创建单个对象,但是这些方式有一个明显缺点:使用同一个接口创建很多对象时,会产生大量的重复代码,因此产生了工厂模式

3.工厂模式:

因为 JavaScript中不能创建类,因此出现了下面这种函数:

function createPerson(name, sex, job) {
  var o = new Object();
  o.name = name;
  o.sex = sex;
  o.job = job;
  o.sayName = function() {
    alert(this.name);
  };
  return o;
}
var person1 = function(“Lucy”, “female”, “nurse”);
var person2 = function(“Tom”, “male”, “engineer”);

不难看出,工厂模式很方便地解决了创建多个相似对象的问题。不过,通过这种方式我们并不能使用 instanceof 操作符明确知道新创建的对象的类型,随之而来的方案是采用构造函数模式

4.构造函数模式:

function Person(name, sex, job) {
  this.name = name;
  this.sex = sex;
  this.job = job;
  this.sayName = function() {
    alert(this.name);
  }
}
var person1 = new Person("Lucy", "female", "nurse");
var person2 = new Person("Tom", "male", "engineer");

对比工厂模式,构造函数模式没有显式地创建对象,直接将属性和方法赋给了 this 对象,并且没有 return 语句。另外,和其他OO 语言相同,构造函数始终都应该以一个大写字母开头,使其能够和非构造函数区分开来。最后,使用构造函数模式需要用到 new 操作符,经历以下四步:

(1)创建一个新对象;

(2)将构造函数的作用域赋给新对象(即 this 指向这个新对象);

(3)执行构造函数总的代码;

(4)返回新对象。

通过构造函数创建的对象会有一个 constructor 属性,该属性指向其构造函数:

person1.constructor == Person //true
person2.constructor == Person //true

对象的constructor 属性最初是用来标识对象类型的,不过,在检测对象类型时,使用 instanceof 操作符更可靠:

person1 instanceof Person //true
person2 instanceof Person //true

至此,我们可以毫不费力的发现在创建对象时,构造函数模式优于工厂模式的一点是构造函数模式解决了工厂模式无法解决的对象识别问题。

虽然构造函数模式相对于工厂模式来说已经是加强版,但也存在自身的缺点,即每个方法都要在每个实例上重新创建一遍。为什么呢?因为在 JavaScript 中,函数同样也是对象,因此每定义一个函数,其实就是实例化了一个对象。将上面的 Person 构造函数写成如下等价形式来方便理解:

function Person(name, sex, job) {
  this.name = name;
  this.sex = sex;
  this.job = job;
  this.sayName = new Function("alert(this.name)");
}

这样看来,每个Person 实例都包含一个不同的 Function 实例。以这种方式创建函数,创建 Function 新实例的机制相同,但会导致不同的作用域链和标识符解析:

person1.sayName== person2.sayName //false

然而,创建两个完全同样任务的 Function 实例的确没有必要,况且有 this 对象在,根本不用在执行代码前就把函数绑定到特定对象上,可以把函数定义转移到构造函数外部来解决该问题:

function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = sayName;
}
function sayName() {
  alert(this.name);
}

这样做解决了两个函数做同一件事的问题,但问题是在全局作用域中定义的函数实际上只能被某个对象调用,而且如果对象需要定义很多方法,那么就要定义很多个全局函数,严重破坏自定义引用类型的封装性,这时,救世主来了——原型模式

5.原型模式:

function Person() {
}
Person.prototype.name = "Lucy";
Person.prototype.sex = "female”;
Person.prototype.job = "nurse";
Person.prototype.sayName = function() {
  alert(this.name);
};
 
var person1 = new Person();
var person2 = new Person();
person1.sayName() //Lucy
person2.sayName() //Lucy

我们创建的每个函数都有一个 prototype 属性,该属性指向一个对象,可以称其为原型对象,使用该对象的好处是可以让所有对象实例共享它所包含的属性和方法。在默认情况下,所有原型对象都会自动获得一个 constructor 属性,这个属性包含一个指向 prototype 属性所在函数的指针。构造函数(Person)、原型对象(Person.prototype)和对象实例(person1, person2)间具体指向关系如图 1 所示:

图-1 指向关系

通过图 1 我们可以看出,person1, person2 都包含一个内部属性,该属性仅仅指向了 Person.prototype,即对象实例与构造函数没有直接关系。虽然在所有实现中都无法访问到 [[Prototype]],但可以通过 isPrototypeOf() 方法来确定对象之间是否存在这种关系:

Person.prototype.isPrototypeOf(person1) //true
Person.prototype.isPrototypeOf(person2) //true

在 ECMAScript 5 中增加了一个方法 Object.getPrototypeOf()在所有支持的实现中,这个方法返回[[Prototype]]的值:

Object.getPrototypeOf(person1) == Person.prototype //true
Object.getPrototypeOf(person1).name //"Lucy"

该方法返回的对象实际就是对象实例所对应的原型对象。使用 Object.getPrototypeOf() 可以方便地取得一个对象的原型,这在利用原型实现继承的情况下很重要,我们会在之后的博客中提到 JavaScript 的继承。

在读取某个对象的某个属性时,都会执行一次搜索,顺序是对象实例(person1)--> 原型对象(Person.prototype) --> 构造函数(Person)。一旦找到相应属性,就停止向上寻找,并返回值。这是多个对象实例共享原型所保存的属性和方法的基本原理。(比如person1 没有 constructor 属性,但是 person1.constructor 依然有值,该值就是找的原型对象 Person.prototype中的constructor 属性,即 person1.constructor--> Person.prototype.constructor)。

虽然可以通过对象实例访问保存在原型对象中的值,但却不能通过对象实例重写原型对象中的值:

person1.name = "Tom";
person1.name //Tom (来自对象实例)
person2.name //Lucy (来自原型对象)

当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性。如果想去除该屏蔽,使用 delete 操作符:

delete person1.name;
person1.name //Lucy (来自原型对象)

通过hasOwnProperty() 方法可以确定什么时候访问的是对象实例属性,什么时候访问的是原型对象属性:

person1.hasOwnProperty("name"); //false
person1.name = "Tom";
person1.hasOwnProperty("true"); //true

in 操作符有两种用法,单独使用和在 for-in 循环中使用。单独使用时,作用类似于 hasOwnProperty() ,区别是只要在通过对象实例能访问给定属性时返回true,无论该属性存在于实例中还是原型中:

person2.name //Lucy (来自原型对象)
"name" in person2 //true
person2.hasOwnProperty("name") //false
person2.name = "Jordan"
"name" in person2 //true
person2.hasOwnproperty("name") //true

要取得对象上所有可枚举的实例属性,可以使用 ECMAScript 5 中的 Object.keys() 方法:

Object.keys(Person.prototype) //"name,sex,job,sayName"
var person1 = new Person();
person1.name = "Duncan";
person1.sex = “male";
Object.keys(person1) //"name,sex"

刚才我们提到的原型模式在每添加一个属性或方法时都要敲一遍 Person.prototype,为减少不必要的输入,同时也从视觉上更好地封装原型的功能,更常见的是采用如下对象字面量的形式重写整个原型对象:

function Person() {
}
Person.prototype = {
  name: "Lucy",
  sex: "female",
  job: "nurse",
  sayName: function() {
    alert(this.name);
  }
};

注:采用对象字面量方式声明虽然最终结果与普通的原型模式相同,但有一个例外——constructor 属性不再指向 Person 了。这也是为什么我们在前面说通过 instanceof 操作符判断对象类型比通过 .constructor属性要更精准:

var newperson1 = new Person();
newperson instanceof Object //true
newperson instanceof Person //true
newperson.constructor == Object //true
newperson.constructor == Person //false

如果 constructor 的值真的很重要,可以主动声明其值:

Person.prototype = {
  constructor: Person,
  name: "Lucy",
  sex: "female",
  job: "nurse",
  sayName: function() {
    alert(this.name);
  }
};

原型具有动态性,这里的动态性是指,我们对原型对象所做的任何修改都能够立即从实例上反映出来,即便我们是先创建的对象实例,之后再修改的原型对象:

var person1 = new Person();
Person.prototype.sayHello = function() {
  alert("hello");
}
person1.sayHello() //"hello"

尽管如此,但如果是重写整个原型对象,那么情况就会有所不同:

function Person() {
}
var person1 = new Person();
Person.prototype = {
  constructor: Person,
  name: "Lucy",
  sex: "female",
  job: "nurse",
  sayName: function() {
    alert(this.name);
  }
};

person1.sayName() //error,报错

这说明,重写原型对象切断了新原型对象与任何已经存在的对象之间的联系,它们引用的仍然是最初的原型对象。

原型模式不仅用来创建自定义类型的对象,同时,它也适用于原生的引用类型(Object, Array, String等)。例如,在 Array.prototype 中可以找到 sort() 方法,在 String.prototype 中可以找到 substring() 方法。可以通过修改原生原型对象定义新的方法:

String.prototype.startsWith = function (text) {
  return this.indexOf(text) ==0;
};
var msg = "Hello Wolrd";
msg.startsWith("Hello"); //true

尽管可以这样做,但并不推荐在产品化的过程中修改原生原型对象,因为这可能导致命名冲突,而且有可能意外地重写原生方法。

原型对象的最大问题是,对于引用类型值的属性很不合适,会引起不想要的对象共享:

Person.prototype = {
  constructor: Person,
  name: "Lucy",
  sex: "female",
  job: "nurse",
  friends: ["Lily", "Tom"],
  sayName: function () {
    alert(this.name);
  }
};
var person1 = new Person();
var person2 = new Person();
person1.friends.push("Jordan");
person2.friends //"Lily, Tom, Jordan"
person1.friends === person2.friends //true

6.构造函数模式和原型模式组合:

为了解决这一问题,可以组合使用构造函数模式和原型模式,这也是创建自定义类型最常见的方式,也是认同度最高的一种创建自定义类型的方法。其中,构造函数模式用于定义实例属性,原型模式用于定义方法和共享的属性:

function Person(name, sex, job) {
  this.name = name;
  this.sex = sex;
  this.job = job;
  this.friends = ["Lily", "Tom"];
}
Person.prototype = {
  constructor: Person,
  sayName: function() {
    alert(this.name);
  }
};
var person1 = new Person("Lucy", "female", "nurse");
var person2 = new Person("Tom", "male", "engineer");
person1.friends.push("Jordan");
person1.friends //"Lily, Tom, Jordan"
person2.friends //"Lily, Tom"
person1.friends === person2.friends //false
person1.sayName === person2.sayName //true

如果你有其他 OO 语言经验,可能更加习惯于在一个构造方法中完成定义,而不是在构造函数之外再初始化原型完成共享属性的定义,这就需要用到动态原型模式

7.动态原型模式:

function Person(name, sex, job) {

this.name = name;

this.sex = sex;

this.job = job;

this.friends = ["Lily","Tom"];

if (typeof this.sayName != "function") {

Person.prototype.sayName =function() {

alert(this.name);

};

}

}

只有在 sayName() 方法不存在的情况下,才会将它添加到原型中。这里对原型对象所做的修改能够立即在对象实例中得到反映。对于采用这种模式创建的对象,同样可以使用 instanceof 操作符确定它的类型。需要注意的是,使用动态原型模式时,不能使用对象字面量重写原型,因为这样会切断现有实例和新原型之间的联系。

如果上述几种模式都不适用的情况下,可以使用寄生构造函数模式

8.寄生构造函数模式:

function Person(name, sex, job) {
 var o = new Object();
 o.name = name;
 o.sex = sex;
 o.job = job;
 o.sayName = function() {
   alert(this.name);
 };
 return o;
}
var person1 = new Person("Lucy", "female", "nurse");
person1.sayName(); //"Lucy"

不难看出,除了使用 new 操作符并把使用的包装函数叫做构造函数之外,该模式跟工厂模式其实完全相同。(构造函数在不返回值的情况下,默认会返回新对象实例,而通过在构造函数的末尾加上 return 语句,可以重写调用构造函数时返回的值)

寄生构造函数模式可以在特殊情况下创建构造函数,比如我们想创建一个具有额外方法的特殊数组,由于不能直接修改 Array 构造函数,因此可以使用该模式:

function SpecialArray() {
 var values = new Array();
 values.push.apply(values, arguments);
 values.toPipedString = function() {
   return this.join(",");
 };
 return values;
}
var colors = newSpecialArray("red", "blue", "green");
colors.toPipedString(); //"red|blue|green"

由于与工厂模式非常相似,因此寄生构造函数模式有着与工厂模式相同的缺点,即无法使用 instanceof 操作符确定对象类型。故在可以使用其他模式的情况下,不建议使用这种模式。

除了上述的几种创建自定义对象的模式,还有一种稳妥构造函数模式。这里有一个稳妥对象的概念,所谓稳妥对象,指的是没有公共属性,而且其方法也不引用 this 对象。稳妥对象最适合在一些安全环境中,或者防止数据被其他应用程序改动时使用:

function Person(name, age, job) {
 var o = new Object();
 //可以在这里定义私有变量和函数
 o.sayName = function() {
   alert(name);
 };
 return o;
}
var person1 = Person("Lucy","female", "nurse");
person1.sayName(); //"Lucy"

在这种模式下,除了使用 sayName() 方法之外,没有其他方法访问 name 的值。稳妥构造函数模式提供的这种安全性,使得它非常适合在某些安全执行环境下使用。要注意的一点是,使用稳妥构造函数模式时同样无法用 instanceof 操作符判断自定义对象的类型。

时间: 2024-09-29 20:08:03

深入JavaScript(一)——JavaScript中的面向对象(一)的相关文章

如何理解并学习javascript中的面向对象(OOP) [转]

如果你想让你的javascript代码变得更加优美,性能更加卓越.或者,你想像jQuery的作者一样,写出属于自己优秀的类库(哪怕是基于 jquery的插件).那么,你请务必要学习javascript面向对象,否则你无法更灵活的使用javascript这门语言. 什么事闭包?到底什么是原型?(知道闭包和原型的,就算得上是javascript的高手了.但真正能够理解,并且灵活运用的人并不多)到底该如何学习javascript中的面向对象呢?在javascript这么语言正如日中天,相信不少人正在为

前端开发:面向对象与javascript中的面向对象实现(一)

前端开发:面向对象与javascript中的面向对象实现(一) 前言: 人生在世,这找不到对象是万万不行的.咱们生活中,找不到对象要挨骂,代码里也一样.朋友问我说:“嘿,在干嘛呢......”,我:“找不到对象!”,他:“就你那样也能找得到对象?”.我一脸黑线...... 废话不多说,今天博主要跟大家聊的是<面向对象与javascript中的面向对象实现>”. 面向对象理解: 面向对象是一种对现实世界理解和抽象的方法,是一种先进的程序设计理念,是一种比较抽象的,多形态的设计模式.我们可以这么理

深入理解javascript中实现面向对象编程方法

介绍Javascript中面向对象编程思想之前,需要对以下几个概念有了解: 1. 浅拷贝和深拷贝:程序在运行过程中使用的变量有在栈上的变量和在堆上的变量,在对象或者变量的赋值操作过程中,大多数情况先是复制栈上的信息,这样就会出现以下情况,如果变量是对象,那么这一操作,复制的只是真正对象所在 的堆内存空间的起始地址,这就是所谓的浅拷贝,如果是深拷贝,则是在内存堆空间中重新分配一个内存,并把分配的内存的起始地址复制过去. 2. 引用类型数据和值类型数据:谈到引用类型数据和值类型数据,自然而然的联想到

前端开发:面向对象与javascript中的面向对象实现(二)构造函数与原型

前端开发:面向对象与javascript中的面向对象实现(二)构造函数与原型 前言(题外话): 有人说拖延症是一个绝症,哎呀治不好了.先不说这是一个每个人都多多少少会有的,也不管它究竟对生活有多么大的影响,单单是自己的念想受到了一定得局限,想法不能够像平地而起的高楼大厦建成一样.可是那大楼也是有烂尾的呀,我觉得最重要的还是外在环境与个人观念的先决条件,决定了拖延症的症状的好坏,有那么一些人,它也有拖延症,但是它在拖的中间,想的更多,看的更远.事情在做的时候更加有条不紊,这拖延症这样看来,它也是好

javascript中的面向对象(object-oriented)编程

本文原发于我的个人博客,经多次修改放到csdn上,主要是做备份用,为了更好的阅读体验,请到我的个人博客上阅读. 最近工作一直在用nodejs做开发,有了nodejs,前端.后端.脚本全都可以用javascript搞定,很是方便.但是javascript的很多语法,比如对象,就和我们常用的面向对象的编程语言不同:看某个javascript开源项目,也经常会看到使用this关键字,而这个this关键字在javascript中因上下文不同而意义不同:还有让人奇怪的原型链.这些零零碎碎的东西加起来就很容

javascript 中的面向对象中比较好的资料

http://www.ruanyifeng.com/blog/2011/06/designing_ideas_of_inheritance_mechanism_in_javascript.html http://www.ruanyifeng.com/blog/2010/05/object-oriented_javascript_encapsulation.html http://www.ruanyifeng.com/blog/2010/05/object-oriented_javascript_

JavaScript中OOP——&gt;&gt;&gt;面向对象中的继承/闭包

  前  言  OOP  JavaScript中OOP-->>>面向对象中的继承/闭包 1.1面向对象的概念 使用一个子类继承另一个父类,子类可以自动拥有父类的属性和方法.      >>> 继承的两方,发生在两个类之间. 1.2JS模拟实现继承的三种方式:        首先,了解一下call/apply/binb:通过函数名调用方法,强行将函数中的this指向某个对象:            call写法:  func.call(func的this指向的obj,参数

JavaScript高级程序设计笔记之面向对象

说起面向对象,大部分程序员首先会想到 类 .通过类可以创建许多具有共同属性以及方法的实例或者说对象.但是JavaScript并没有类的概念,而且在JavaScript中几乎一切皆对象,问题来了,JavaScript中如何面向对象? JavaScript中将对象定义为:一组无序的 键值对的集合,属性以及方法的名称就是键,键的值可以是任何类型(字符串,数字,函数--) 在JavaScript中,所有对象继承自Object,所有对象继承自Object,所有对象继承自Object! 创建 1  简单创建

JavaScript高级程序设计学习笔记--面向对象程序设计

工厂模式 虽然Object构造函数或对象字面量都可以用来创建单个对象,但这些方式有个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码.为解决这个问题,人们开始使用 工厂模式的一种变体. function createPerson(name,age,job){ var o=new Object(); o.name=name; o.age=age; o.job=job; o.sayName=function(){ alert(this.name); }; return o; } var

浅析JavaScript和PHP中三个等号(===)和两个等号(==)的区别

先做个简单的介绍,让先有个直观的认识 == equality 等同 === identity 恒等 == 两边值类型不同的时候,要先进行类型转换,再比较. === 不做类型转换,类型不同的一定不等. 举例说明: "1" == true 类型不同,"=="将先做类型转换,把true转换为1,即为 "1" == 1: 此时,类型仍不同,继续进行类型转换,把"1"转换为1,即为 1 == 1: 此时,"==" 左