技术开发 频道

JavaScript面向对象编程探讨:实战代码

        【IT168 技术】JavaScript是一个类C的语言,相对于C++/Java比较奇怪,相当的强大,本文主要从一个整体的角度来说明一下JavaScript的面向对象的编程。这篇文章主要基于ECMAScript 5,旨在介绍新技术。关于兼容性的东西,请看最后一节。

  初探

  我们知道JavaScript中的变量定义基本如下:

var name = 'Chen Hao';;

  var email
= 'haoel(@)hotmail.com';

  var website
= 'http://coolshell.cn';

  

  如果要用对象来写的话,就是下面这个样子:

var chenhao = {

  name :
'Chen Hao',

  email :
'haoel(@)hotmail.com',

  website :
'http://coolshell.cn'

  };
  

  于是,我就可以这样访问:

  //以成员的方式

chenhao.name;

  chenhao.email;

  chenhao.website;

  //以hash map的方式

chenhao["name"];

  chenhao[
"email"];

  chenhao[
"website"];

  

  关于函数,我们知道JavaScript的函数是这样的:

var doSomething = function(){

  alert(
'Hello World.');

  };
  

  于是,我们可以这么干:

var sayHello = function(){

  var hello
= "Hello, I'm "+ this.name

  
+ ", my email is: " + this.email

  
+ ", my website is: " + this.website;

  alert(hello);

  };

  

  //直接赋值,这里很像C/C++的函数指针

chenhao.Hello = sayHello;

  chenhao.Hello();

  相信这些东西都比较简单,大家都明白了。可以看到JavaScript对象函数是直接声明,直接赋值,直接就用了。runtime的动态语言。

  还有一种比较规范的写法是:

  //我们可以看到, 其用function来做class。

var Person = function(name, email, website){

  this.name
= name;

  this.email
= email;

  this.website
= website;

  this.sayHello
= function(){

  var hello
= "Hello, I'm "+ this.name + ", \n" +

  
"my email is: " + this.email + ", \n" +

  
"my website is: " + this.website;

  alert(hello);

  };

  };

  var chenhao
= new Person("Chen Hao", "haoel@hotmail.com",

  
"http://coolshell.cn");

  chenhao.sayHello();

  顺便说一下,要删除对象的属性,很简单:

delete chenhao['email']

  上面的这些例子,我们可以看到这样几点:

  Javascript 的数据和成员封装很简单。没有类完全是对象操作。纯动态!

  Javascript function中的this指针很关键,如果没有的话,那就是局部变量或局部函数。

  Javascript 对象成员函数可以在使用时临时声明,并把一个全局函数直接赋过去就好了。

  Javascript 的成员函数可以在实例上进行修改,也就是说不同实例相同函数名的行为不一定一样。

  属性配置 – Object.defineProperty

  先看下面的代码:

 //创建对象

  var chenhao
= Object.create(null);

  
//设置一个属性

  
Object.defineProperty( chenhao,

  
'name', { value: 'Chen Hao',

  writable:
true,

  configurable:
true,

  enumerable:
true });

  
//设置多个属性

  
Object.defineProperties( chenhao,

  {

  
'email' : { value: 'haoel@hotmail.com',

  writable:
true,

  configurable:
true,

  enumerable:
true },

  
'website': { value: 'http://coolshell.cn',

  writable:
true,

  configurable:
true,

  enumerable:
true }

  }

  );

  下面就说说这些属性配置是什么意思。

  writable:这个属性的值是否可以改。

  configurable:这个属性的配置是否可以改。

  enumerable:这个属性是否能在for…in循环中遍历出来或在Object.keys中列举出来。

  value:属性值。

  get ()/set (_value):get 和 set 访问器。

  Get/Set访问器

  关于get/set访问器,它的意思就是用 get/set 来取代 value(其不能和 value 一起使用),示例如下:

var age = 0;

  
Object.defineProperty( chenhao,

  
'age', {

  
get: function() {return age+1;},

  
set: function(value) {age = value;}

  enumerable :
true,

  configurable :
true

  }

  );

  chenhao.age
= 100; //调用set

  alert(chenhao.age);
//调用get 输出101(get中+1了);

  我们再看一个更为实用的例子——利用已有的属性(age)通过get 和 set 构造新的属性(birth_year):

Object.defineProperty( chenhao,

  
'birth_year',

  {

  
get: function() {

  var d
= new Date();

  var y
= d.getFullYear();

  return ( y
- this.age );

  },

  
set: function(year) {

  var d
= new Date();

  var y
= d.getFullYear();

  this.age
= y - year;

  }

  }

  );

  alert(chenhao.birth_year);

  chenhao.birth_year
= 2000;

  alert(chenhao.age);

  这样做好像有点麻烦,你说,我为什么不写成下面这个样子:

var chenhao = {

  name:
"Chen Hao",

  email:
"haoel@hotmail.com",

  website:
"http://coolshell.cn",

  age:
100,

  
get birth_year() {

  var d
= new Date();

  var y
= d.getFullYear();

  return ( y
- this.age );

  },

  
set birth_year(year) {

  var d
= new Date();

  var y
= d.getFullYear();

  this.age
= y - year;

  }

  };

  alert(chenhao.birth_year);

  chenhao.birth_year
= 2000;

  alert(chenhao.age);

  是的,你的确可以这样的,不过通过defineProperty ()你可以干这些事:

  1)设置如writable,configurable,enumerable 等这类的属性配置。

  2)动态地为一个对象加属性。比如:一些 HTML 的 DOM 对像。

  查看对象属性配置

  如果查看并管理对象的这些配置,下面有个程序可以输出对象的属性和配置等东西:

//列出对象的属性.

  
function listProperties(obj)

  {

  var newLine
= "
"
;

  var names
= Object.getOwnPropertyNames(obj);

  
for (var i = 0; i < names.length; i++) {

  var prop
= names[i];

  document.write(prop
+ newLine);

  
// 列出对象的属性配置(descriptor)动用getOwnPropertyDescriptor函数。

  var descriptor
= Object.getOwnPropertyDescriptor(obj, prop);

  
for (var attr in descriptor) {

  document.write(
"..." + attr + ': ' + descriptor[attr]);

  document.write(newLine);

  }

  document.write(newLine);

  }

  }

  listProperties(chenhao);

  call,apply,bind和this

  关于 Javascript 的 this 指针,和C++/Java 很类似。 我们来看个示例:(这个示例很简单了,我就不多说了)

function print(text){

  document.write(this.value
+ ' - ' + text+ '
'
);

  }

  var a
= {value: 10, print : print};

  var b
= {value: 20, print : print};

  print(
'hello');// this => global, output "undefined - hello"

  a.print(
'a');// this => a, output "10 - a"

  b.print(
'b'); // this => b, output "20 - b"

  a[
'print']('a'); // this => a, output "10 - a"

  我们再来看看call和apply,这两个函数的差别就是参数的样子不一样,另一个就是性能不一样,apply 的性能要差很多。(关于性能,可到 JSPerf 上去跑跑看看)

print.call(a, 'a'); // this => a, output "10 - a"

  print.call(b,
'b'); // this => b, output "20 - b"

  print.apply(a, [
'a']); // this => a, output "10 - a"

  print.apply(b, [
'b']); // this => b, output "20 - b"

  但在 bind 后,this 指针,可能会有不一样,但是因为 Javascript 是动态的。如下面的示例

var p = print.bind(a);

  p(
'a'); // this => a, output "10 - a"

  p.call(b,
'b'); // this => a, output "10 - b"

  p.apply(b, [
'b']); // this => a, output "10 - b"

  继承和重载

  通过上面的那些示例,我们可以通过 Object.create ()来实际继承,请看下面的代码,Student 继承于 Object。

var Person = Object.create(null);

  
Object.defineProperties

  (

  Person,

  {

  
'name' : { value: 'Chen Hao'},

  
'email' : { value : 'haoel@hotmail.com'},

  
'website': { value: 'http://coolshell.cn'}

  }

  );

  Person.sayHello
= function () {

  var hello
= "

Hello, I am
"+ this.name + ",
" +

  
"my email is: " + this.email + ",
"
+

  
"my website is: " + this.website;

  document.write(hello
+ "
"
);

  }

  var Student
= Object.create(Person);

  Student.no
= "1234567"; //学号

  Student.dept
= "Computer Science"; //

  
//使用Person的属性

  document.write(Student.name
+ ' ' + Student.email + ' ' + Student.website +'
'
);

  
//使用Person的方法

  Student.sayHello();

  
//重载SayHello方法

  Student.sayHello
= function (person) {

  var hello
= "

Hello, I am
"+ this.name + ",
" +

  
"my email is: " + this.email + ",
"
+

  
"my website is: " + this.website + ",
"
+

  
"my student no is: " + this. no + ",
"
+

  
"my departent is: " + this. dept;

  document.write(hello
+ '
'
);

  }

  
//再次调用

  Student.sayHello();

  
//查看Student的属性(只有 no 、 dept 和 重载了的sayHello)

  document.write(
'

' + Object.keys(Student) + '
'
);

 

  通用上面这个示例,我们可以看到,Person 里的属性并没有被真正复制到了 Student 中来,但是我们可以去存取。这是因为 Javascript 用委托实现了这一机制。其实,这就是 Prototype,Person 是 Student 的 Prototype。

  当我们的代码需要一个属性的时候,Javascript 的引擎会先看当前的这个对象中是否有这个属性,如果没有的话,就会查找他的 Prototype 对象是否有这个属性,一直继续下去,直到找到或是直到没有 Prototype 对象。

  为了证明这个事,我们可以使用 Object.getPrototypeOf ()来检验一下:

 Student.name = 'aaa';

  
//输出 aaa

  document.write(
'

' + Student.name + '

');

  
//输出 Chen Hao

  document.write(
'

' +Object.getPrototypeOf(Student).name + '

');

  于是,你还可以在子对象的函数里调用父对象的函数,就好像 C++ 里的 Base::func () 一样。于是,我们重载 hello 的方法就可以使用父类的代码了,如下所示:

//新版的重载SayHello方法

  Student.sayHello
= function (person) {

  
Object.getPrototypeOf(this).sayHello.call(this);

  var hello
= "my student no is: " + this. no + ",
"
+

  
"my departent is: " + this. dept;

  document.write(hello
+ '
'
);

  }

  这个很强大吧。

  组合

  上面的那个东西还不能满足我们的要求,我们可能希望这些对象能真正的组合起来。为什么要组合?因为我们都知道是这是 OO 设计的最重要的东西。不过,这对于 Javascript 来并没有支持得特别好,不好我们依然可以搞定个事。

  首先,我们需要定义一个 Composition 的函数:(target 是作用于是对象,source 是源对象),下面这个代码还是很简单的,就是把 source 里的属性一个一个拿出来然后定义到 target 中。

 function Composition(target, source)

  {

  var desc
= Object.getOwnPropertyDescriptor;

  var prop
= Object.getOwnPropertyNames;

  var def_prop
= Object.defineProperty;

  prop(source).forEach(

  
function(key) {

  def_prop(target, key, desc(source, key))

  }

  )

  return target;

  }

 

 有了这个函数以后,我们就可以这来玩了:

//艺术家

  var Artist
= Object.create(null);

  Artist.sing
= function() {

  return this.name
+ ' starts singing...';

  }

  Artist.paint
= function() {

  return this.name
+ ' starts painting...';

  }

  
//运动员

  var Sporter
= Object.create(null);

  Sporter.run
= function() {

  return this.name
+ ' starts running...';

  }

  Sporter.swim
= function() {

  return this.name
+ ' starts swimming...';

  }

  Composition(Person, Artist);

  document.write(Person.sing()
+ '
'
);

  document.write(Person.paint()
+ '
'
);

  Composition(Person, Sporter);

  document.write(Person.run()
+ '
'
);

  document.write(Person.swim()
+ '
'
);

  
//看看 Person中有什么?(输出:sayHello,sing,paint,swim,run)

  document.write(
'

' + Object.keys(Person) + '
'
);

  Prototype和继承

  我们先来说说 Prototype。我们先看下面的例程,这个例程不需要解释吧,很像C语言里的函数指针,在C语言里这样的东西见得多了。

var plus = function(x,y){

  document.write( x
+ ' + ' + y + ' = ' + (x+y) + '
'
);

  return x
+ y;

  };

  var minus
= function(x,y){

  document.write(x
+ ' - ' + y + ' = ' + (x-y) + '
'
);

  return x
- y;

  };

  var operations
= {

  
'+': plus,

  
'-': minus

  };

  var calculate
= function(x, y, operation){

  return operations[operation](x, y);

  };

  calculate(
12, 4, '+');

  calculate(
24, 3, '-');

 

  那么,我们能不能把这些东西封装起来呢,我们需要使用 prototype。看下面的示例:

var Cal = function(x, y){

  this.x
= x;

  this.y
= y;

  }

  Cal.prototype.operations
= {

  
'+': function(x, y) { return x+y;},

  
'-': function(x, y) { return x-y;}

  };

  Cal.prototype.calculate
= function(operation){

  return this.operations[operation](this.x, this.y);

  };

  var c
= new Cal(4, 5);

  c.calculate(
'+');

  c.calculate(
'-');

  这就是 prototype 的用法,prototype 是 javascript 这个语言中最重要的内容。网上有太多的文章介始这个东西了。说白了,prototype 就是对一对象进行扩展,其特点在于通过“复制”一个已经存在的实例来返回新的实例,而不是新建实例。被复制的实例就是我们所称的“原型”,这个原型是可定制的(当然,这里没有真正的复制,实际只是委托)。上面的这个例子中,我们扩展了实例 Cal,让其有了一个 operations 的属性和一个 calculate 的方法。

  这样,我们可以通过这一特性来实现继承。还记得我们最最前面的那个 Person 吧, 下面的示例是创建一个 Student 来继承 Person。

function Person(name, email, website){

  this.name
= name;

  this.email
= email;

  this.website
= website;

  };

  Person.prototype.sayHello
= function(){

  var hello
= "Hello, I am "+ this.name + ",
"
+

  
"my email is: " + this.email + ",
"
+

  
"my website is: " + this.website;

  return hello;

  };

  
function Student(name, email, website, no, dept){

  var proto
= Object.getPrototypeOf;

  proto(Student.prototype).constructor.call(this, name, email, website);

  this.no
= no;

  this.dept
= dept;

  }

  
// 继承prototype

  Student.prototype
= Object.create(Person.prototype);

  
//重置构造函数

  StudentStudent.prototype.constructor
= Student;

  
//重载sayHello()

  Student.prototype.sayHello
= function(){

  var proto
= Object.getPrototypeOf;

  var hello
= proto(Student.prototype).sayHello.call(this) + '
'
;

  hello
+= "my student no is: " + this. no + ",
"
+

  
"my departent is: " + this. dept;

  return hello;

  };

  var me
= new Student(

  
"Chen Hao",

  
"haoel@hotmail.com",

  
"http://coolshell.cn",

  
"12345678",

  
"Computer Science"

  );

  document.write(me.sayHello());

 

  兼容性

  上面的这些代码并不一定能在所有的浏览器下都能运行,因为上面这些代码遵循 ECMAScript 5 的规范,关于 ECMAScript 5 的浏览器兼容列表,你可以看这里“ES5浏览器兼容表”。

  本文中的所有代码都在 Chrome 最新版中测试过了。

  下面是一些函数,可以用在不兼容 ES5 的浏览器中:

Object.create ()函数

  
function clone(proto) {

  
function Dummy() { }

  Dummy.prototype
= proto;

  DummyDummy.prototype.constructor
= Dummy;

  return
new Dummy(); //等价于Object.create(Person);

  }

  var me
= clone(Person);

  defineProperty ()函数

  
function defineProperty(target, key, descriptor) {

  
if (descriptor.value){

  target[key]
= descriptor.value;

  }
else {

  descriptor.get
&& target.__defineGetter__(key, descriptor.get);

  descriptor.set
&& target.__defineSetter__(key, descriptor.set);

  }

  return target

  }

  keys ()函数

  
function keys(object) { var result, key

  result
= [];

  
for (key in object){

  
if (object.hasOwnProperty(key)) result.push(key)

  }

  return result;

  }

  
Object.getPrototypeOf() 函数

  
function proto(object) {

  return !
object? null

  :
'__proto__' in object? object.__proto__

  :
/* not exposed? */ object.constructor.prototype

  }

  bind函数

  var slice
= [].slice

  
function bind(fn, bound_this) { var bound_args

  bound_args
= slice.call(arguments, 2)

  return
function() { var args

  args
= bound_args.concat(slice.call(arguments))

  return fn.apply(bound_this, args) }

  }

 

  

0
相关文章