JS基础:面向对象编程

目录

一. 类与对象
 1. ES5里的构造函数(constructor)与对象的生成
 2. ES6里的类(class)与对象的生成
  2.1 构造方法
  2.2 属性
  2.3 方法
二. 类的继承
三. this关键字和super关键字
 1. OC里的self关键字
 2. OC里的super关键字
 3. JS里的this关键字
 4. JS里的super关键字


一. 类与对象


JS对象和我们OC对象是一样的概念,都是类的一个实例。对象是单个实物的抽象,而类则是对象的抽象,类里面定义了该类对象的属性和方法,并提供了初始化方法来生成对象。

我们移动端的OC和Java语言都有类和对象的概念,但是在ES5里却只有对象而没有类的概念,想要创建一个对象是通过构造函数来创建的,类这个概念是在ES6里才引入的,下面我们简单了解一下。

1. ES5里的构造函数(constructor)与对象的生成

在ES5里,没有类的概念,我们想要创建一个类,需要通过构造函数来创建,但是我们完全可以把构造函数当作一个类来看待。所谓构造函数,其实也是一个普通函数而已,只不过我们约定了一些它的特定写法:

  • 函数名首字母大写
  • 函数体内部使用this关键字,代表该类的对象
// 构造函数
function Person() {
    // 该类的属性
    this.name = null;
    this.sex = null;
    this.age = 0;

    // 该类的方法
    this.eat = function () {
        console.log(this.name + this.sex + this.age + '吃饭喽!');
    }
}

我们使用new关键字来创建一个对象new关键字的作用是根据后面的构造函数生成一个对象并返回,如果不使用new关键字,那么构造函数真得就是一个普通的函数了,它不具备创建对象和返回对象的能力)

// 创建一个对象
let person = new Person();
// 给对象的属性赋值
person.name = '张三';
person.sex = '男';
person.age = 11;
// 调用对象的方法
person.eat();

我们简单了解一下构造函数就可以了,因为ES6里面引入了类的概念,更符合我们的理解与使用习惯,所以实际开发中我们直接使用类就可以了。

2. ES6里的类(class)与对象的生成

注意:同样的,在ES6里我们也是使用new关键字来创建一个对象,后面就不说了。

ES6里面引入了类的概念,使得我们可以通过class关键字来创建一个类,现在我们就来学学它这个类要怎么编写。

先回想下咱们OC,要编写一个类,类里面有什么内容呀?无非就是三部分嘛:初始化方法、该类对象的属性和方法,如果把后两者再细分一下,属性无非可以有公开属性和私有属性,方法可以有公开方法和私有方法、类方法和实例方法,那沿着这个思路去学JS的类怎么写不就得了嘛。

class Person {
    // 公开属性
    name;
    sex;
    age;
    friends = [];

    // 私有属性
    #height;
    #weight;

    // 类属性
    static country = '中国';

    // 构造函数
    constructor() {

    }

    // 公开方法
    eat() {
        console.log(this.name + '吃饭饭');
    }

    // 私有方法
    #cry() {
        console.log(this.name + '哭泪泪');
    }

    // 类方法
    static payTax() {
        console.log('纳税');
    }
}
2.1 构造方法(constructor方法)

constructor方法类似于我们OC里的- init方法,我们完全可以照着- init方法来理解它,那它有几个作用呢?

  • 用来创建一个对象并返回,因此它是一个类必须有的一个方法,要不然你怎么生成对象呀,即便你不写,系统也会给你默认一个。再细说一下,在OC里,当我们调用new方法来创建某个类的实例时,就会触发该类的- init方法来完成对象的创建并返回。类似的,在JS里,当我们调用new关键字来创建某个类的实例时,就会触发该类的constructor方法来完成对象的创建并返回。

  • 用来完成属性的初始化,当然我们也可以根据需求重写构造函数。(详见下文2.2属性的初始化)

2.2 属性

属性,有定义、初始化、使用三个基本操作。

  • 定义:如var a;
  • 初始化:如var a = 11;或赋值a = 11;
  • 使用:略......

在OC里,我们会把公开属性定义在.h文件里,把私有属性定义在.m文件的延展里,如有需要我们会在- init方法里完成这些属性的初始化,然后就可以使用这些属性了。

那在JS里呢?

  • 属性的定义

公开属性直接定义在类的内部即可。

class Person {
    // 定义公开属性
    name;
    sex;
    age;
    friends = [];
}

私有属性和公开属性一样,直接定义在类的内部即可。那如何来表明一个属性是一个私有属性呢?我们只需要在属性名前面加一个#作为属性名的一部分就可以表明这个属性是一个私有属性了,这样这个属性就只能在类的内部使用

class Person {
    // 定义公开属性
    name;
    sex;
    age;
    friends = [];

    // 定义私有属性
    #height;
    #weight;
}
  • 属性的初始化

在OC里,如果我们想要初始化某些属性,需要在- init方法里完成。JS里也是一样的,我们需要在constructor方法里完成属性的初始化。

class Person {
    // 定义公开属性
    name;
    sex;
    age;
    friends = [];

    // 定义私有属性
    #height;
    #weight;

    // 构造函数
    constructor() {
        // 初始化属性
        this.name = '张三';
        this.sex = '男';
        this.age = 11;
        this.friends = [];
        this.#height = 177;
        this.#weight = 67;
    }
}

JS里除了可以在构造方法里初始化属性之外,还可以直接在定义属性的时候完成初始化,这样我们就不用在构造方法里再写一遍属性来初始化了嘛,这个好像比上面那个简洁一些。

class Person {
    // 定义公开属性
    name = '张三';
    sex = '男';
    age = 11;
    friends = [];

    // 定义私有属性
    #height = 177;
    #weight = 67;
    
    // 构造函数
    constructor() {

    }
}

当然如果我们想要根据外界传进来的值对某些属性进行初始化,类比OC的重写- init方法,JS里我们可以重写constructor方法。

class Person {
    // 定义公开属性
    name;
    sex;
    age;
    friends = [];

    // 定义私有属性
    #height;
    #weight;

    // 构造函数
    constructor(name, sex, age) {
        // 初始化属性
        this.name = name;
        this.sex = sex;
        this.age = age;
    }
}

注意:想想咱们OC里,一般对基本数据类型不管它们的初始化,而几个类型是需要初始化一下的,否则用不了。

  • 属性的使用

和OC一样,用点语法.对属性进行读取和写入就可以了。

class Person {
    // 定义公开属性
    name;
    sex;
    age;
    friends = [];

    // 定义私有属性
    #height = 177;
    #weight = 67;

    // 构造函数
    constructor() {
        console.log('我们是私有属性:%d, %d', this.#height, this.#weight);// 类的内部才可以访问私有属性
    }
}

// 创建一个对象
let person = new Person();
// 使用属性
person.name = '张三';
person.sex = '男';
person.age = 11;
person.friends = ['李四', '王五'];
// person.#height = 177;// 类的外部无法访问私有属性
// person.#weight = 67;
console.log(person);
  • 类属性(或者叫静态属性),你能信?

OC里听说过类方法(或者叫静态方法),但没有听说过类属性(或者叫静态属性)啊,JS里竟然有类属性这么个东西,用法就是在对象属性前面加上static关键字修饰就可以了。

class Person {
    // 定义类属性
    static country = '中国';
}

console.log(Person.country);// 中国
2.3 方法

方法,有声明、定义、调用三个基本操作。

这里解释一下这三个概念:

  • 声明:声明就是告诉编译器有这么一个方法,比如我们OC在.h文件里声明一个方法。
  • 定义:定义是指方法的具体实现,比如我们OC在.m文件里实现一个方法。
  • 调用:略......

那在JS里呢?

在JS里,因为所有函数的定义都会被提升到代码头部,所以我们不需要专门声明一个函数,直接定义和调用就可以了。那为一个类添加一个方法,类比到OC里就相当于省略掉.h文件里声明这一步,直接在这个类的内部写这个函数的实现就可以了,而且不需要在这个函数前面加function关键字,然后等着外界调用就行了。

  • 方法的实现

公开方法。

class Person {
    name = '张三';

    // 公开方法
    eat() {
        console.log(this.name + '吃饭饭');
    }
}

私有方法也是在方法名前面加一个#作为方法名的一部分就可以了,这样这个方法就只能在类的内部使用。

  • 方法的调用

用点语法.调用方法就可以了。

class Person {
    name = '张三';

    // 公开方法
    eat() {
        console.log(this.name + '吃饭饭');
        this.#cry();// 类的内部才可以调用私有方法
    }

    // 私有方法
    #cry() {
        console.log(this.name + '哭泪泪');
    }
}

// 创建一个对象
let person = new Person();
person.eat();
// person.#cry();// 类的外部不能调用私有方法
  • 类方法(或者叫静态方法)

在一个方法前面加上static关键字,这个方法就成了一个类方法。

class Person {
    // 类方法
    static payTax() {
        console.log('纳税');
    }
}

Person.payTax();// 纳税


二. 类的继承


JS通过extends关键字来实现类的继承。

class Person {

}

class Boy extends Person {

}

很简单吧,上述代码就表示Boy类继承自Person类。不过在编写类的继承时,我们要特别注意构造函数的写法。类比OC,我们可以得到一个关于构造函数需要注意的点

  • 如果子类不重写父类的构造方法,是没有问题的,创建子类的时候无非是默认调用父类的构造方法嘛。
class Person {
    name;

    // 构造方法
    constructor(name) {
        this.name = name;
    }

    eat() {
        console.log(this.name + '吃');
    }
}

class Boy extends Person {
    girlFriend;

    playGame() {
        console.log(this.girlFriend + '陪' + this.name + '玩游戏');
    }
}

let boy = new Boy('张三');
console.log(boy);// Boy {name: "张三", girlFriend: undefined}
  • 关键来了,如果子类有自己的构造方法——即重写了父类的构造方法,那子类的构造方法里就必须首先调用一下super()方法,来完成子类继承于父类那部分资源的初始化,然后再做子类自己自定义的内容。(关于super关键字,详见第三部分)
class Person {
    name;

    // 构造方法
    constructor(name) {
        this.name = name;
    }

    eat() {
        console.log(this.name + '吃');
    }
}

class Boy extends Person {
    girlFriend;

    // 重写父类的构造方法
    constructor(name, girlFriend) {
        super(name);// 必须首先调用一下super()方法,其实这里的super()就是父类的构造方法

        this.girlFriend = girlFriend;
    }

    playGame() {
        console.log(this.girlFriend + '陪' + this.name + '玩游戏');
    }
}

let boy = new Boy('张三', '花花');
console.log(boy);// Boy {name: "张三", girlFriend: "花花"}
  • 同时,我们也看到只有当一个类(如Person类)不是继承别的类而来的,它的构造方法里才可以直接写自定义的内容,继承而来的类(如Boy类)的构造方法里绝对得首先调用super()方法。


三. this关键字和super关键字


现在我们不妨来回顾一下OC里的self关键字和super关键字,这将有助于我们对照理解JS里的this关键字和super关键字。

1. OC里的self关键字
1   @implementation Person
2  
3   - (void)test1 {
4    
5   }
6
7   - (void)test2 {
8     
9       NSLog(@"%@", self.name);
10    
11      [self test1];
12  }
13
14  @end
  • 第3行和第7行,定义了两个实例方法。
  • 第9行打印name属性使用了self关键字,第11行使用self关键字调用test2方法,那这两个self关键字怎么解释呢?
  • 网上最常见的说法为self关键字代表当前方法的调用者,那套用这句话,我们来解释一下。代码走到第9行,我们看到了self关键字,那这时我们会自然而然地把当前方法认作是test2方法,没问题,此时self关键字指的就是test2方法的调用者——外部定义的某个Person对象。代码走到第11行,我们又看到了self关键字,这个时候突然有些懵逼!诶?这里的当前方法是指谁啊?是test1呢还是test2?当然了,其实这个当前方法无论是test1还是test2,都不会影响的最终结果,因为它们俩的调用者是同一个对象,可令人不爽的就是确定当前方法是谁,这不是很重要但思维上无法省略的一步,我必须明确地知道当前方法到底是谁。
  • 好,看来网上关于self关键字最常见的说法不是那么准确,因此我自己给它改了一小下,self关键字代表它当前所在方法的调用者,它是一个对象,拿着这句话去解释上面的第9行和第11行就很轻松和具有一致性了,可见仅仅是把当前方法改成了它当前所在方法,就非常便于我们理解。
@implementation Person

+ (void)test1 {
    
}

+ (void)test2 {
    
    NSLog(@"%@", self);
    
    [self test1];
}

@end

此外,我们也知道self关键字可以用在类方法中,那上面代码的self关键字指的就是Person类。

总结:

  • 在实例方法中,self关键字代表当前类的某个实例对象。
  • 在类方法中,self关键字代表当前类。
  • 万变不离其宗,还是记住一句话,self关键字代表它当前所在方法的调用者,它是一个对象。
2. OC里的super关键字

通过上面的回顾,我们知道self关键字要么代表一个实例对象,要么代表一个类,总之self关键字是一个对象,可以用来作为消息接收者。

super关键字可不是一个对象啊,我们不能想当然地认为super关键字是某个对象的父类对象。super关键字仅仅是一个编译器指示符,作用是告诉当前消息接收者去它的父类里去查找方法的实现,而不是在当前类中查找,super关键字的消息接受者其实还是self

@implementation Son : Father

- (instancetype)init {
    
    // [super init]的作用:完成子类继承于父类那部分资源的初始化
    // 为什么要给self赋值[super init]:为了防止父类的初始化方法release掉了,根本完不成子类继承于父类那部分资源的初始化,这样如果self为空的话就没必要执行子类自定义的内容了
    self = [super init];
    if (self != nil) {
        
        NSLog(@"%@", NSStringFromClass([self class]));
        NSLog(@"%@", NSStringFromClass([super class]));
    }
    return self;
}

@end

上面两个都输出Son类。

题目中,self调用class方法最终会转化为objc_msgSend(self, @selector(class))的调用,而super调用class方法最终会转化为objc_msgSendSuper(self, @selector(class))的调用,两者的消息接受者是相同的,都是self,只不过前者是直接在当前类中找class方法的实现,后者是去父类中找class方法的实现,然而当前类和父类class方法的实现都是一样的,都是返回当前对象所属的类,那self当前所在方法的调用者肯定是某个Son对象,所以两者都会输出Son类。

所以请不要把[super class][self superclass]弄混了,后者肯定打印Father类的。

3. JS里的this关键字

this关键字是一个非常重要的语法点。毫不夸张地说,不理解它的含义,大部分开发任务都无法完成。this关键字和咱们OC里的self关键字是一模一样的概念,都代表它当前所在方法的调用者,它是一个对象。

  • this关键字用在实例方法中,代表当前类的某个实例对象。
class Person {
    name;

    test1() {

    }

    test2() {
        console.log(this.name);

        this.test1();
    }
}
  • this关键字用在类方法中,代表当前类。
class Person {
    static name;

    static test1() {

    }

    static test2() {
        console.log(this.name);

        this.test1();
    }
}
4. JS里的super关键字

先撂这一句话:JS里的super关键字既可以当作对象使用,也可以当作方法使用,它不同于OC的里super关键字——是个编译器指示符。

第一种情况,super关键字当作对象使用时,令人惊喜的是,它还真像我们想当然的那样:

  • super关键字出现在子类的实例方法里时,代表父类的原型对象prototype
  • super关键字出现在子类的类方法里时,代表父类。
class Parent {
    static myMethod(msg) {
        console.log('static', msg);
    }

    myMethod(msg) {
        console.log('instance', msg);
    }
}

class Child extends Parent {
    static myMethod(msg) {
        super.myMethod(msg);// 这里的super代表Parent类,这句代码等价于Parent.myMethod(msg);
    }

    myMethod(msg) {
        super.myMethod(msg);// 这里的super代表Parent的原型对象prototype,这句代码等价于Parent.prototype.myMethod(msg);
    }
}


Child.myMethod(11); // static 11

let child = new Child();
child.myMethod(12); // instance 12

我们来简单解释一下上面出现的一个概念——父类的原型对象prototypeprototype是类的一个属性,在JS里我们每创建一个类,系统都会自动为这个类添加一个prototype属性,而这个属性其实是该类的一个对象,它拥有该类所有的属性和方法,但是我们一般不会去直接使用它,而是把它晾在一个高台上拿一杆旗杵在那,它存在的意义就是一个模板,告诉将来该类所有实例化出来的对象都和它长得一样。

第二种情况,super关键字当作方法使用时,它只能出现在子类的构造函数里,而且还必须得出现,这里的super()方法就代表父类的构造方法,你把super()方法写到别的地方或者子类的构造函数里不写其实这里的super()方法就是父类的构造方法,都会报错的。

class Parent {}

class Child extends Parent {
    constructor() {
        super();// 这里的super()方法就代表调用一下父类Parent的构造方法
    }
}

你可能感兴趣的:(JS基础:面向对象编程)