JS面向对象编程笔记(一):原型、原型链
# 开头
笔记视频内容源自B站JavaScript从入门到放弃 第十二章 面向对象编程 (opens new window),笔记为自行整理复习使用,欢迎一同学习交流,转载请通知原作者
# 一、原型、原型链
# 1.1、什么是面向对象?
面向对象编程是目前主流的的一个编程的模式,它将我们真实世界各种复杂的关系抽象成了一个个的对象,然后由这些对象的分工合作完成真实世界的模拟。每一个对象都有其功能中心,即完成的任务(方法,属性)。因此面向对象编程(OOP)
具有灵活、代码可复用、高度模块化等特点。
1.对象是单个实物的抽象
2.对象是一个容器,封装了对应的属性和方法,属性是对象的状态,方法是对象的行为(完成的任务)
# 1.2、构造函数实例化对象
生成一个对象通常需要一个模板(表示一类实物的共同特征),让对象生成
类(class)
就是对象的模板,但是js不是基于类的,而是基于构造函数(constructor)
和原型链(prototype)
,我们来看一段代码:
<body>
<script>
function Dog(name, age) {
//name和age就是当前实例化对象的属性
this.name = name;
this.age = age;
}
var dog1 = new Dog("阿黄", 10);
</script>
</body>
2
3
4
5
6
7
8
9
10
11
这个Dog()
是构造函数,为了与普通函数区别,构造函数的名字的第一个字母通常都大写
构造函数的特点:
- 函数体内使用
this
关键字,代表了所要生成的对象实例 - 生成对象必须通过
new
关键字实例化
如果没有new
关键字,Dog函数为普通函数没有返回值,结果就是undefined
<body>
<script>
function Dog(name, age) {
//name和age就是当前实例化对象的属性
this.name = name;
this.age = age;
}
// var dog1 = new Dog("阿黄", 10);
var d = Dog("阿黄", 10);
console.log(d);
</script>
</body>
2
3
4
5
6
7
8
9
10
11
12
13
使用严格模式
报错会进一步扩大:
<body>
<script>
function Dog(name, age) {
'use strict'
this.name = name;
this.age = age;
}
var d = Dog("阿黄", 10);
console.log(d);
</script>
</body>
2
3
4
5
6
7
8
9
10
11
# 1.3、instanceof用法
instanceof
表示当前对象是否是它的类的实例,如果是返回true,不是返回false
<body>
<script>
function Dog(name){
if(!(this instanceof Dog)){
//this指向了window,外部没有使用关键字new
return new Dog(name)
}
//this指向当前实例,外部使用了关键字new
this.name = name;
}
var d1 = Dog('阿黄')
console.log(d1);
</script>
</body>
2
3
4
5
6
7
8
9
10
11
12
13
14
上面的代码利用instanceof
关键字来判断,如果是作为普通函数进来,this
的指向为window而不是Dog,返回一个new过后的构造函数,如果是则赋值其name属性
# 1.4、new命令原理
当我们使用new
命令时发生了什么呢?我们使用一段代码来研究一下:
<body>
<script>
function Person(name){
this.name = name
}
var p1 = new Person();
console.log(p1);
console.log(p1.__proto__ === Person.prototype) //true
console.log(Person.prototype);
</script>
</body>
2
3
4
5
6
7
8
9
10
11
首先new关键字构造函数后:
创建一个空对象,作为将要返回的对象实例
将空对象的原型对象指向了构造函数的
prototype
属性对象
3.将这个实例对象的值赋值给函数内部的this
关键字
4.指向构造函数体内的代码
# 1.5、constructor属性
constructor
是在每个对象创建时都会自动的拥有的一个构造函数属性
<body>
<script>
function Person(name){
this.name = name
}
var p1 = new Person('chen');
console.log(p1);
console.log(p1.constructor);
console.log(p1.constructor === Person);
console.log(Person.prototype);
</script>
</body>
2
3
4
5
6
7
8
9
10
11
12
13
constructor
继承自原型对象,其中指向了构造函数的引用,所以p1.constructor === Person
应该为true
使用构造函数创建对象的好处在于,能够共享内部属性和方法:
<body>
<script>
function Person(name) {
this.name = name;
this.sayName = function () {
console.log(this.name);
};
}
var p1 = new Person('Tom');
var p2 = new Person('Jack');
console.log(p1,p2);
</script>
</body>
2
3
4
5
6
7
8
9
10
11
12
13
14
就像p1和p2,它们都各自用有自己的name属性。但同样的也有弊端:每个实例调用的时候方法都要重新创建,存在冗余,想要解决这类方法要使用**原型对象(prototype)**来解决
# 1.6、原型对象介绍
先看代码:
<body>
<script>
function Foo() {}
Foo.prototype.name = "chen";
Foo.prototype.showName = function () {
console.log("fchen");
};
var f1 = new Foo();
var f2 = new Foo();
console.log(f1.name);
console.log(f2.name);
f1.showName();
f2.showName();
</script>
</body>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
原型对象:Foo.prototype
实例对象:f1就是实例对象,实例对象可以构造多个,每一个原型对象中都有一个__proto__
,每个实例对象都有一个constructor
属性,这个constructor通过继承关系继承来的,它指向了当前的构造函数Foo
构造函数:用来初始化新创建对象的函数,Foo就是构造函数,自动给构造函数赋予一个属性prototype
,该属性指向了实例对象的原型对象(__proto__
)
# 1.7、原型链
<body>
<script>
function Foo(){};
var f1 = new Foo();
</script>
</body>
2
3
4
5
6
以上面的代码结合图示来分析一下Foo构造函数的原型链:
Foo的实例对象f1,它的原型对象f1.__proto__
指向了Foo的prototype
属性,f1的constructor
属性间接继承于Foo,如果将Foo.prototype
也当作一个实例化的对象,同样的他也会有__proto__
与constructor
属性,Foo.prototype.__proto__
指向了Object.prototype
,``Foo.prototype.constructor**直接继承**于
Foo`,如图所示:
控制台与图示一致,f1.__proto__
=== Foo.prototype
再深入下去,将Foo当成一个实例对象,它的__proto__
又将指向哪一个呢:
Object实例对象的__proto__
为null
把Foo和Object再当成一个实例,它们也有对应的constructor
__proto__
也同理:
函数也是一个对象,任何函数我们可以看作是Function所new出来的对象,如果我们把Foo当作对象它同样的有__proto__
和constructor
属性
再再深究下去Funtion实例对象也有其__proto__
和constructor
属性
Foo里面的constructor是继承而来的,继承的当前构造函数的原型
这就是原型链,是不是很绕(我也晕晕的),研究的就是原型对象
# 1.8、prototype属性的作用
js的继承就是通过prototype
来进行的
什么是js的继承机制?
<body>
<script>
function Foo() {}
var f1 = new Foo();
Foo.prototype.name = "chen";
console.log(f1.name);
</script>
</body>
2
3
4
5
6
7
8
看结果:
可以看到,name属性并不在f1上,而是通过继承挂载在f1的原型对象(__proto__)
上
<body>
<script>
function Foo() {}
var f1 = new Foo();
var f2 = new Foo();
Foo.prototype.name = "chen";
console.log(f1.name);
console.log(f2.name);
Foo.prototype.name = "chen2";
console.log(f1.name);
console.log(f2.name);
</script>
</body>
2
3
4
5
6
7
8
9
10
11
12
13
14
可以看到两个实例都会被修改
JS继承机制,通过原型对象(__proto__
)实现继承。原型对象的作用,就是定义了所有的实例对象共享的属性和方法
读取属性和方法的规则:js引擎会先寻找对象本身的属性和方法,如果找不到就到它的原型对象去找,如果还是找不到,就到原型的原型去找,如果直到最顶层的Object.prototype
还是找不到,就会返回undefined
如果对象和它的原型,都定制了同名的属性,那么优先读取对象自身的属性,这也叫覆盖
<body>
<script>
function Foo() {}
var f1 = new Foo();
var f2 = new Foo();
Object.prototype.showName = function(){
console.log(24);
}
f1.showName()
var arr = [12,3,4]
arr.showName()
</script>
</body>
2
3
4
5
6
7
8
9
10
11
12
13
所有的对象的尽头都是Object
接下来我们编写一段代码,让我们自己的构造函数拥有数组的部分属性和方法:
<body>
<script>
function MyArray() {}
MyArray.prototype = Array.prototype;
var arr = new MyArray();
console.log(arr);
arr.push(1,2,3)
console.log(arr);
</script>
</body>
2
3
4
5
6
7
8
9
10
但是上面的代码同时也改变了他的constructor,修改成了数组的constructor,怎么样能不改变构造函数的指向呢
要添加这一句:
MyArray.prototype.constructor = MyArray;
我们打印一下arr.constructor
总结:一旦我们修改构造函数的原型对象,为防止引用出现问题,同时也要修改原型对象的constructor
属性
constructor属性表示原型对象和构造函数之间的关联关系
# 1.9、总结
function Foo(){};
var f1 = new Foo();
2
构造函数:Foo
实例对象:f1
原型对象:Foo.prototype
1.原型对象和实例对象的关系
console.log(Foo.prototype === f1.__proto__);
2.原型对象和构造函数的关系
console.log(Foo.prototype.constructor === Foo);
3.实例对象和构造函数
间接关系
是实例对象可以继承远程对象的constructor
属性
console.log(f1.constructor === Foo);
Foo.protptype={};
console.log(Foo.prototype === f1.__proto__);
console.log(Foo.prototype.constructor === Foo);
2
3
所以,代码的顺序很重要