一、ES5的类实现
由于js并不是真正面向对象的语言,所以在ES6规范之前,一直没有"类"的概念,但是可以用变通的方式实现。最常用的就是利用构造函数。
function Person(name,age)
{
this.name = name;
this.age = age;
this.sayName = function(){
console.log("name:"+this.name);
}
}
var person1 = new Person("tcy",20);//new出新的对象
分析这段代码:
1、创建一个构造函数Person,我们可以认为是"类",其中有两个类属性(name,age)和一个类方法(sayName)。
2、采用new实例化Person,返回一个对象person1,我们认为是"类对象"。
这段代码已经很接近java的"类"了,但是Person还是个function,那么这个构造函数和普通的函数有什么区别呢。
有以下两点区别:
1、在函数内部对新对象(this)的属性进行设置,通常是添加属性和方法。
2、构造函数可以包含返回语句(不推荐),但返回值必须是this,或者其它非对象类型的值。
继承是面向对象的一种重要模式,我们看如何实现,我们来看一种继承的实现方式-原型继承。
我们先来了解下什么叫做原型,打印下Person构造函数对象
Person构造函数中还有个隐含的_proto_方法,包含了两个默认方法(constructor和_proto_)。我们描述下结构图,可以看到,_proto_指向了Person的原型Person.prototype,而原型中的constructor又指向了Person。
再来测试下,在Person.prototype原型对象上添加两个方法sayName,sayAge。
function Person(name,age)
{
this.name = name;
this.age = age;
this.sayName = function(){
console.log("object name:"+this.name);
}
}
Person.prototype.sayName= function(){
console.log("prototype name:"+this.name);
}
Person.prototype.sayAge= function(){
console.log("prototype age:"+this.age);
}
var person1 = new Person("tcy",20);//new出新的对象
再打印下Person原型对象,增加了两个方法。
结构如下:
接下来我们继续看下这段代码
var person1 = new Person("tcy",20);//new出新的对象
console.log(person1.constructor===Person)//true
console.log(person1.constructor===Person.prototype.constructor)//true
person1.sayName()//object name:tcy
person1.sayAge()//prototype age:20
首先new出一个Person的实例对象person1。实际上内部分三步执行的
第一步是建立一个新对象person1;
第二步将该对象(person1)内置的原型对象设置为构造函数(就是Person)prototype 属性引用的那个原型对象;
第三步就是将该对象(person1)作为this 参数调用构造函数(就是Person),完成成员设置等初始化工作。
非常晦涩,看不懂,没关系,我们继续上图。内置的原型对象也叫_proto_, _proto_指向Person.prototype。
实例对象person1就是通过内置的原型对象实现对构造函数(Person)原型的访问(person1.可以理解为person1._proto.)所以下面的执行结果就理解了。
console.log(person1.constructor===Person)//true
console.log(person1.constructor===Person.prototype.constructor)//true
通过constructor,person1实现了对构造函数所有的属性和方法的的访问。
我们再来看下属性和方法的寻址方式,优先在其构造方法(Person)中查找,如果构造方法中存在,就不会继续到其原型查找(例如sayName方法),如果不存在该方法或者属性,则继续在其原型查找(如sayAge()),所以执行结果如下:
person1.sayName()//object name:tcy
person1.sayAge()//prototype age:20
理解了原型的原理,我们来看下回归正题,如何用原型实现继承。
//父类
function Person(name,age)
{
this.name = name;
this.age = age;
this.sayName = function(){
console.log("object name:"+this.name);
}
}
//子类
function Worker(job){
this.job = job;
this.sayJob = function(){
console.log("object job:"+this.job);
}
}
Worker.prototype = new Person("tcy",20);//利用原型实现Worker对Person的继承
var teacher = new Worker("teacher");
teacher.sayName();//object name:tcy
teacher.sayJob();//object job:teacher
Worker类的原型设置成Person的类对象,按照寻址的规则,Worker继承了Person的方法(sayName方法)。有兴趣的道友们可以把这个原型关系图画出来。
二、class类
ES6中提供了class关键字,对"类"的支持越来越接近面对对象语言。我们看下它是怎么使用的。
class Person
{
constructor(name, age) {
this.name = name;
this.age = age;
}
sayName(){
console.log("object name:"+this.name);
}
}
var person = new Person("tcy",20);
person.sayName();//object name:tcy
class关键字定义Person这个类,constructor为该类的构造方法,如果不定义构造方法,则默认增加一个;sayName为类方法,注意,该方法不需要增加function标识。使用new创建一个实例对象person时,会自动调用constructor构造方法,传入入参,返回实例对象,this指向的就是该实例对象。
我们打印下person对象。
对比下我们前面讲的原型可以看到,ES6的class定义类,其实只是用原型实现类的一个"马甲"而已。类方法其实就是原型方法,等价于:
Person.prototype ={
constructor(){},
sayName(){}
}
三、私有方法和属性
在java语法中,使用private表示私有方法和属性,但是ES6中没有定义该关键字,事实上,ES6也没有提供私有方法和需求的方法,只能通过变通的方式。
那么对于私有的方法和属性的标准是什么呢,需要满足一下要求
1、 class内部不同方法间可以使用,因此this要指向实例化对象(必须)
2 、不能被外部访问,因此实例化对象person.name既不能获得值,也不能设定值,应该返回undefined,甚至应该在实例化之后,并不知道有name这个属性存在,(必须)
3 不能被继承,因此extends后子类不具备该属性(必须)
4 方便的调用方式,比如类this._name形式(备选)
const _name = Symbol('name');
const _age = Symbol('age');
class Person {
constructor(name, age) {
this[_name] = name;
this[_age] = age;
}
sayName(){
console.log("object name:"+this[_name]);
}
}
var person = new Person("tcy",20);
person.sayName();
person._name;//undefined
我们利用Symbol值的唯一性,将私有属性的名字命名为一个Symbol值,这个属性能在类内部访问(sayName能调用name属性),但是无法被实例对象访问(person._name为undefined)。实际上这种方法是有缺陷的,我们后面会讲到。
理论上讲,私有属性和私有方法的区别是,私有方法是函数,实现私有属性的方式也是适合私有方法的。
const _name = Symbol('name');
const _age = Symbol('age');
const _sayName = Symbol('sayName');
class Person {
constructor(name, age) {
this[_name] = name;
this[_age] = age;
}
//私有方法
[_sayName](){
console.log("object name:"+this[_name]);
}
//共有方法
sayName(){
this[_sayName]();
}
}
var person = new Person("tcy",20);
person.sayName();//object name:tcy
person._sayName();//报错
person._name;//undefined
四、静态属性和方法
类方法前加上static关键字,表示这个方法可以用类直接调用,而无需使用类对象,该方法就是静态方法
class Person
{
constructor(name, age) {
this.name = name;
this.age = age;
}
static sayName(){
console.log("this is static functon");
}
}
Person.sayName();
由于不用类对象调用,所以也就无法用到this(如用到,也是指类)。
那么对于静态属性,是不是可以在属性前增加static表示呢,很遗憾,不可以。ES6对于类的属性实现只有两种方式表示
var sex = 0
class Person
{
//第一种,最常用的,在constructor,使用.操作符
constructor(name, age) {
this.name = name;
this.age = age;
}
//第二种,使用get set来定义属性
get sex() {
return sex
}
set sex(value) {
sex = value
}
static sayName(){
console.log("this is static functon");
}
}
Person.sayName();
var person = new Person("tcy",20);
person.sex = 1;
console.log(person.sex);//1
第一种,是最常用的,在constructor,使用.操作符;第二种,使用get set来定义属性,有点曲线救国的意思。
我们可以利用第二种方式,变通实现私有属性。
var sex = 0
class Person
{
//第一种,最常用的,在constructor,使用.操作符
constructor(name, age) {
this.name = name;
this.age = age;
}
//第二种,使用get set来定义属性
static get sex() {
return sex
}
static set sex(value) {
sex = value
}
static sayName(){
console.log("this is static functon");
}
}
Person.sayName();
Person.sex = 1;
console.log(Person.sex);//1
五、总结
本篇主要讲了class类的相关原理和基本知识,下一章节重点介绍类的另一个重要特性-继承。