JavaScript-面向对象编程
原型
对于大多数语言,面向对象编程都是通过类
和实例
来实现.但是在JS中,它不区分类和实例,而是通过原型(prototype)
来实现面向对象编程.
举个例子来说明一下原型
:
假设我们想创建xiaoming
这个具体的学生,但是我们没有Student
类型可以用,不过有一个现成的对象
1 |
|
这个robot
对象有名字,有身高还会跑,有点像xiaoming
,于是就是用它来创建
xiaoming吧.直接把它改名为Student
,然后创建出xiaoming
1 |
|
JS的原型链
没有class
的概念,所有对象都是实例,所谓的继承关系
就是一个对象的原型指向另一个对象.
比如如果你把xiaoming
的原型指向其他对象:
1 |
|
**注意:**上面的
__proto__
只是为了演示而已,实际开发不要直接用obj.__proto__
去改变一个对象的原型
object.create()
可以传入一个原型对象,并创建一个基于该原型的新对象,但是新对象什么属性都没有.因此我们可以写个函数来创建xiaoming
1 |
|
原型链
JS对每个创建的对象都会设置一个原型
,指向它的原型对象
.
当我们用
obj.xxx
访问一个对象的属性时,JS引擎会先在该对象上找对应的属性,没有找到就去它的原型对象上找,还没有找到,就一直上溯到object.prototype
对象,都没找到,就返回undefined
例如一个array
对象
1 |
|
这就是它的原型链
1 |
|
Array.prototype
定义了indexOf()
,shift()
等方法,因此你可以在所有Array
对象上直接调用这些方法.
当我们创建一个函数,函数也是对象.
1 |
|
Function.prototype
定义了apply()
等方法,所以所有函数都能调用这些方法.
创建对象
object.create()
如上所示.这种方法允许你创建一个新对象,并将现有对象作为其原型.这意味着新对象将继承现有对象的属性和方法.
字面量
最简单,最常用.就是直接使用{...}
1 |
|
new Object()
不太常用,主要用于创建一个空对象,然后动态地添加属性
1 |
|
构造函数
这种方法允许你创建多个具有相同属性和方法的对象.构造函数使用this
引用新创建的对象.
1 |
|
new
关键字把构造函数内的this
绑定到对应的新对象上.- 构造函数默认返回
this
,也就是不用写return this
原型链是这样的:
person1
的原型指向函数Person
的原型
1 |
|
使用new <构造函数>
创建的对象还会从原型上获取一个constructor
属性,指向<构造函数>
本身.
1 |
|
- 红色箭头是原型链
Person.prototype
指向的就是person1
和person2
的原型对象
- 这个原型对象自己有个
constructor
的属性,指向Person
构造函数本身 - 另外
person1
,person2
是没有prototype
属性的,不过可以用__proto__
这个非标准用法来查看 - 此时我们就认为
person1
,person2
“继承”自Person
另外还有一个小问题:
1 |
|
新创建的对象,属性不相等是正常的,但是他们的方法也是不相等的
.也就是说它们各自会有各自的方法,几时方法的名字和代码都一样.
但是这样子同样的方法存两份有点浪费资源了,所以我们可以在共享对象上创建这个方法
,然后继承它的对象就会使用同一个方法.
1 |
|
如果一个函数被定义为用于创建对象的
构造函数
,但是调用时忘记写new
:
strict
模式下,this.name = name
会报错,因为this
绑定为undefined
- 非
strict
模式下,this.name = name
不会报错,因为this
绑定为window
,无意间创建了全局变量name
,并返回undefined
所以调用构造函数千万不要忘记了
new
.一些语法检查工具可以帮助你检查你的语法,比如jslint
但其实new
可以通过函数封装,一个常用的编程模式像这样:
1 |
|
类语法
ES后引入创建对象的语法,以后会提及.
目前字面量和类语法最常用.
构造函数
的缺点是其原型链的设置方式容易出错,而且没有类语法清晰
Object.create()
主要用于精确控制原型链的场景,或者需要创建不带构造函数的对象
模拟继承
在传统基于Class的语言如Java,C++中,继承的本质是扩展一个已有的class,并生成新的Subclass.由于这类语言严格区分类和实例,继承实际上类型
的扩展.
但是JS采用原型继承
,所以无法直接扩展一个Class,因为根本没有Class这个概念.
那如果想要继承扩展呢?
1 |
|
原型链如下:
此时如果想要基于Student
扩展出PrimaryStudent
1 |
|
在这个例子中,
Student.call(this, props)
做了以下事情:
- 调用 Student 函数: 它像普通函数一样调用了
Student
构造函数。- 设置 this 的值: call() 的第一个参数指定了
Student
函数内部this
的值。 在这里,this
指的是正在被创建的PrimaryStudent
实例。 这很重要,因为在Student
构造函数内部,this.name = props.name || 'Unnamed';
这行代码会将name
属性添加到this
指向的对象上。 通过将this
设置为PrimaryStudent
的实例,我们确保name
属性被添加到PrimaryStudent
实例上,而不是其他地方。- 传递参数:
call()
的第二个参数props
将被传递给Student
函数。 这使得Student
构造函数可以使用props
对象来初始化PrimaryStudent
实例的name
属性
这个做法可以理解为:
在PrimaryStudent
构造函数内部手动调用Student
构造函数,并将其this
指向了PrimaryStudent
的实例.这样模拟了Student
构造函数被用于创建PrimaryStudent
实例的效果,模拟”继承”.
但实际上这并不是真正意义上的继承
.举这个例子只是为了扩展,实际中并不会这么操作.
但我看不懂他下面的解释,只摘录这个例子,后面的理解找的AI.
原型继承
JavaScript继承的核心是原型链
.每个对象都有一个__proto__
属性.(现代浏览器中一般通过Object.getPrototypeOf()来访问
),它指向创建该对象的函数的prototype
属性.当访问对象的某个属性时,JS引擎会现在对象自身查找,如果没有找到,就会沿着原型链向上查找,知道找到该属性或到达原型链顶端(null).
__proto__
VS prototype
prototype
(原型属性)
是一个
函数对象
才拥有的属性.它指向原型对象
.使用该函数(构造函数)new
出来的对象,它的__proto__
属性会被设置为prototype
的属性值一般用来定义通过构造函数创建的对象的
共享属性
和方法
.也就是说它充当新对象的模板.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function Person(name) {
this.name = name;
}
Person.prototype.greet = function() {
console.log('Hello, my name is ' + this.name);
};
const person1 = new Person('Alice');
const person2 = new Person('Bob');
person1.greet(); // Output: Hello, my name is Alice
person2.greet(); // Output: Hello, my name is Bob
console.log(person1.greet === person2.greet); // true, 不同实例共享同一个方法Person.prototype
是Person
构造函数的原型属性
__proto__
(原型链)
是一个
对象
拥有的属性(包括函数对象).它指向创建该对象的构造函数的prototype
属性.是JS实现原型继承的关键.它用来构成JS的
原型链
.当访问一个对象属性的时候,JS引擎会现在对象自身找该属性,如果找不到,会沿着__proto__
指向的原型对象继续查找,知道找到或者到尽头(null)为止.但现在我们一般不直接调用它,他是用
Object.getPrototypeOf()
来查看;Object.setPrototypeOf()
来设置对象的原型.1
2
3console.log(person1.__proto__ === Person.prototype); // Output: true
console.log(person1.__proto__.greet === Person.prototype.greet); // Output: true
console.log(Object.getPrototypeOf(person1) === Person.prototype); // Output: true (preferred way)
inherits
inherits
函数的目标是建立两个构造函数之间的继承关系,使一个构造函数的实例
能够继承另一个构造函数的原型上的属性和方法
.
1 |
|
此时F
和Parent
指向同一个
原型对象
new F()
通过F()
构造函数创建的实例(f_instance),它的__proto__
指向它的构造函数的prototype
属性,也就是F
的prototype
属性,因为F
和Parent
指向同一个
原型对象,所以f_instance的__proto__
实际上也是指向Parent.prototype
此时把Child的prototype
指向F构造函数创建的实例(f_instance
),因为f_instance.__proto__
指向Parent.prototype
,此时Child和Parent就串起来了.
可是此时Child的prototype的constructor属性
应该是F()
,因为此时Child的prototype指向了f_instance,所以Child的prototype的constructor指向的是f_instance的构造函数,也就是F().
所以要修复回来,指回Child
. Child.prototype.constructor = Child;
最终原型链大概是这样:
真尼玛复杂….
我觉得大概理一下就行了,反正以后都是用ES6的class语法
以上复杂的动作可以用一个inherits()
函数封装起来,隐藏F
的定义,简化代码.
1 |
|
然后调用这个inherits()
函数就能简单实现原型链继承
1 |
|
Class
JS的对象模型基于原型实现,虽说它概念简单,没有类的概念.但是理解起来复杂,实现起来也麻烦.ES6引入了class
这个语法糖,让类的定义更加简单.
1 |
|
Class继承
直接通过extends
实现
1 |
|
class
: 定义类extends
: 表示原型链对象来自Student
constructor
: 定义子类的构造函数,子类需要name
和grade
两个参数,name
用于super(name)
中调用父类的构造函数,否则父类的name
属性无法初始化
ES6引入的
class
只是一个语法糖,实际的底层原理跟上面的没有任何区别.我只能说…舒服了….