目录
一. 类与对象
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
我们来简单解释一下上面出现的一个概念——父类的原型对象prototype
:prototype
是类的一个属性,在JS里我们每创建一个类,系统都会自动为这个类添加一个prototype
属性,而这个属性其实是该类的一个对象,它拥有该类所有的属性和方法,但是我们一般不会去直接使用它,而是把它晾在一个高台上拿一杆旗杵在那,它存在的意义就是一个模板,告诉将来该类所有实例化出来的对象都和它长得一样。
第二种情况,super
关键字当作方法使用时,它只能出现在子类的构造函数里,而且还必须得出现,这里的super()
方法就代表父类的构造方法,你把super()
方法写到别的地方或者子类的构造函数里不写其实这里的super()方法就是父类的构造方法,都会报错的。
class Parent {}
class Child extends Parent {
constructor() {
super();// 这里的super()方法就代表调用一下父类Parent的构造方法
}
}