JavaScript 使用函数来定义类而不是像一些其它语言那样通过 class 关键字来定义类,并通过类和原型来完成面向对象编程,目前,基于类的面向对象语言是面向对象世界里的主流。那可以使用 JavaScript 定义类,为类添加方法( Method )和属性( Property ),以及应用继承( Inherit )。
JavaScript 使用类来完成面向对象编程,类是使用构造器来定义的,而构造器使用 JavaScript 函数来实现。
类由两部分组成:类声明和类体。类声明由构造函数来定义,函数体就是类体,类的属性和方法都在类体中定义。
可以在类体内定义属性。若要创建属性,最简单的方法是使用 this 关键字定义变量的方式 。
可以在类体内定义方法。若要创建方法,可以使用 function 关键字创建匿名函数定义,将匿名函数赋给一个变量,并且使用 this 关键字引用该变量。
所有的构造器都是类,但是,并非所有的类都是构造器。内建的类都是使用构造器创建的类,但是宿主对象(例如 window 、 navigator )可能不是。
当对象被抽象为类之后,就可以创建具体的实例来操作了。实例是该类的一个具体的个体,例如,根据 Person 类创建该类的一个实例,名为 John ,然后定义 John 的身高、头发颜色等特性,也可以执行跑动等行为。
每个实例都是其所属类的一个精确而又不同的个体,在 OOP 编程中,类的实例被称为对象。由于对象是类的实例,所以创建对象的操作称为实例化。
虽然大多数面向对象开发语言都使用类来完成面向对象编程,但是应该澄清的是,类不是面向对象编程的实质性内涵。
面向对象编程的实质性内涵是将所有的业务逻辑单元都视为一个对象(显然,该对象是抽象化的);并且,类也不是唯一的用来完成面向对象编程的方法,例如, JavaScript 使用构造器和原型来完成面向对象开发。
不能将面向对象误解为面向类,实际上很多用户已经深陷在这样的误区之中,一想到面向对象编程,马上就想到类。要从根本上走出这个误区,否则不会有真正的面向对象编程。
面向对象程序设计是一种围绕真实世界的概念来组织模型的程序设计方法,它使用对象来描述问题空间的实例。关于对象这一概念,目前还没有统一的定义,一般认为,对象是现实世界物体特征的抽象个体,它反映了系统为之保存信息和(或)与它交互的能力。它是一些属性及功能的一个封装体,在程序设计领域,可以用以下公式表示:
对象 = 数据 + 作用于这些数据上的操作(算法)。
类是具有相同操作功能(方法)和数据(属性)的对象的集合(某些程序语言也允许定义事件,不过,事件也可以看作是一种实现特殊功能的方法),是抽象后的集合,它规定了其中对象的共有的属性和方法;对象是类的一个实例,例如,自行车是一个类,而道路上的某一个自行车则是一个对象。
对象和类的关系相当于一般的程序设计语言中变量和变量类型的关系。所以,类有时也被称为一种数据类型,它可以看作抽象数据类型(数据类型被抽象后的表示形式)的具体实现。这个时候,数据类型是指数据的集合和作用于其上的操作的集合,而抽象数据类型不关心操作实现的细节。从外部看,类型的行为可以用新定义的操作加以规定。
消息是向某对象请求服务的一种表达方式。对象内有方法和数据(属性),外部的用户或对象对该对象提出服务请求(调用方法或访问属性),可以称为向该对象发送消息。
在创建了类之后,就可以使用类了,要使用类,首先必须创建该类的一个实例,一般会将实例作为值赋给一个变量。在创建类的一个实例后,该实例就会捆绑该类中定义的方法和属性了,然后就可以调用方法和属性了。类的实例也被称为对象。
属性是类中声明的变量,与其它地方变量的声明基本相同,只是属性必须属于 this 关键字,并且这里没有使用 var 关键字。例如前面为 Person 类声明了两个属性 age 和 nickName :
this.age;
this.nickName;
在类中定义的函数称为方法,方法是类中封装运算的一种工具,以下就是方法的定义语法:
this.MethodName = function(arg_1, arg_2,..., arg_n){
// 执行一些业务逻辑运算语句
// 最后使用 return 语句返回一个值 ( 也可以不返回值 )
return "$1 些数据或者变量 ";
}
在使用时,先是 this 关键字,之后的点语法连接的是方法名,就像是声明一个匿名函数,只不过属于 this 关键字。
参数 arg_1 、 arg_n 等都是该方法使用的参数,参数之间使用逗号隔开,这也被称为参数列表( parametersList )。在参数后的花括号 {} ,即为整个方法内容。方法如果返回值,使用 return 可将值返回。
例如,前面为 Person 类声明了一个 showInfo 方法:
this.showInfo = function () {
return '我的名字是 ' + this.nickName + ' ,我现在 ' + this.age + '了。 ';
};
属性的初始化
当为类定义一个属性时,可以为属性赋值,称为属性的初始化。属性在定义时也可以没有初始值。例如下面的类定义:
function Person() {
this.age;
this.nickName;
this.showInfo = function () {
return '我的名字是 ' + this.nickName + ' ,我现在 ' + this.age + ' 了。 ';
};
}
由于两个属性都没有赋初始值,那么在访问属性时都会是 undefined :
var tom = new Person();
alert(tom.nickName == undefined); // true
alert(tom.age == undefined); // true
// 这时,如果要使用属性,最好首先为属性赋初始值,就像前面介绍的那样:
var tom = new Person();
// 为属性赋初始值
tom.nickName = 'Tom';
tom.age = 22;
在类体内使用 this 关键字
在类内,如果要访问该类定义的方法或属性,必须使用 this 关键字,它表示类的实例自身,使用它可以引用当前代码所在类的特定实例。 this 的行为与引用当前类的实例的变量类似。
关键字 this 不可以省略,例如改变一下代码,实际是声明了两个私有属性(私有属性仅能在类内使用):
function Person(myName, myAge) {
nickName = myName;
age = myAge;
}
注意,在方法内嵌入的函数形成一个新的运行环境,在该内嵌函数内使用 this 并不会指向对象的实例,而是指向全局对象 window 。
例如下面的代码在方法内嵌入一个函数 test() ,那么其中的 this 表示的是全局对象 window ,而非 Foo 的实例 foo :
function Foo() {}
Foo.prototype.method = function () {
function test() {
alert(this); // this 在这个函数里是全局对象
}
test();
};
var foo = new Foo();
foo.method();
// 要在嵌入的函数内使用 this , 那么最好是用一个中间变量,
// 例如下面的代码, 使用 that 作为中间变量:
function Foo() {}
Foo.prototype.method = function () {
var that = this;
function test() {
alert(that === foo);
// that 在这个函数里是 foo 对象
}
test();
};
var foo = new Foo();
foo.method();
对于静态方法,存在相同的问题,例如下面的代码:
function Foo() {}
Foo.method = function () {
// 注意这里定义一个静态方法
function test() {
alert(this); // this 在这个函数里是全局对象
}
test();
};
Foo.method();
// 也可以使用中间变量来解决:
function Foo() {}
Foo.method = function () {
var that = this;
function test() {
alert(that === Foo); // that 在这个函数里是 foo 类
}
test();
};
Foo.method();
以上就是 js 中的类了。当然,不过是构造函数的使用,接下来就是真正的类了
class 类名 {
[属性定义];
[方法定义];
}
在构造函数中使用 this 来定义属性,但必须初始给值
类的构造函数使用 constructor 作为函数名。
无论什么类,构造函数迷人返回实例的对象( this )
class AClass {
d; // 在类中的顶层定义的属性,声明
constructor(a, b, c) {
// 定义构造函数,并在其中使用 this 关键字定义实例属性
this.a = a;
this.b = b;
this.c = c;
}
printIn() {
let s = `a = ${this.a}; b = ${this.b}; c = ${this.c};`;
console.log(s);
return s;
}
}
从 ES6类的定义格式来看,类和构造函数似乎有很大的不同,但事实上, ES6类本身其实就是一个函数,这一点可以通过使用 typeof 来验证。另外, ES6类自身指向的就是构造函数,所以 ES6类完全可以看作是构造函数的另外一种写法。
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。为了阻止实例的继承,可以在方法名前面添加 static 关键字。添加了 static 关键字后的方法称为静态方法。原型方法需要通过对象实例来调用才能执行,如果使用类名来调用原型方法,则方法不会被执行;静态方法则只能通过类名来调用,不能使用实例名来调用,否则会报错。 私有属性和私有方法
ES6不提供直接的私有方法和私有属性实现,只能通过变通方法模拟实现。目前在类中定义私有方法和私有属性的方案有多种,常用的如:通过在变量/方法名前加下划线“_”前缀的命名约定方法,和使用 ES6提供的 Symbol 来定义私有方法和私有属性等方案。但使用约定名称来定义私有方法和私有属性的方法是不可靠的,因为通过命名来约定的私有变量和私有方法其实还是公有的,它们在类的外部仍然可以被直接访问。而通过 Symbol 来定义私有变量和私有方法,主要是利用了 Symbol 的唯一性来将私有方法和私有属性的名字命名为一个 Symbol 值。但同样的,使用 Symbol 定义私有方法和私有属性这种方案也不是完全百分之百可靠的,因为通过 Reflect.ownKeys(),在类的外部依然可以获得私有方法名和私有属性名。现在有一种更简单可靠的提案,就是在属性名和方法名前面添加“#”修饰符来定义私有方法和私有属性。 属性的 getter 和 setter 方法
对象实例属性值的设置除了可以使用构造函数外,最常用的方法是使用 setter (设值)方法;而非私有属性的访问则除了使用对象直接访问属性外,也常常使用属性的 getter (取值)方法。对于私有属性来说,通常都会定义对应的 setter 和 getter 方法。
面向对象方式编程主要是为了代码的重用,如果一个元素在页面中只需要出现一次,则使用过程式编程实现该元素的功能会更简洁。