文章目录
1.概述
在典型的面向对象编程(OOP)的语言中,如Java,都存在类的概念,类就是对象的模版,对象就是类的示例,但在ES6之前,JS中并没有引入类的概念。
在ES6之前,对象不是基于类创建的,而是用一种称为构建函数的特殊函数来定义对象和它们的特征。
创建对象可以通过如下三种方式:
- 对象字面量
- new Object()
- 自定义构造函数
<script>
// 1.字面量创建
var obj1 = {};
// 2.new 创建
var obj2 = new Object();
// 3.构造函数
function Person(name, age) {
this.name = name;
this.age = age;
this.eat = function () {
console.log('吃饭');
}
}
var xy = new Person('xy', 20);
xy.eat();
</script>
2.构造函数
构造函数是一种特殊的函数,主要用来初始化对象,即为对象成员变量赋初始值,它总与new一起使用,我们可以把对象中一些公共属性和方法抽取出来,然后到这个函数里面。
- 构造函数用于创建某一类对象,其首字母要大写
- 构造函数要和new一起使用才有意义
new在执行时会做四件事:
- 在内存中创建一个新的空对象
- 让this指向这个新的对象
- 执行构造函数里面的代码,给这个新对象添加属性和方法
- 返回这个新对象(所以构造函数里面不需要return)
2.1 实例成员
实例成员就是构造函数内部通过this添加的成员,如下的name、age就是实例成员。
注意:实例成员只能通过实例化的对象来访问
<script>
function Person(name, age) {
this.name = name;
this.age = age;
this.eat = function () {
console.log('吃饭');
}
}
var xy = new Person('xy', 20);
// 1.实例成员 name、age
console.log(xy.name);//xy
console.log(Person.name);//Person,不能访问到
</script>
显示截图:
2.2 静态成员
静态成员就是在构造函数本身上添加的成员 如下的sex
注意:静态成员只能通过构造函数来访问
<script>
function Person(name, age) {
this.name = name;
this.age = age;
this.eat = function () {
console.log('吃饭');
}
}
var xy = new Person('xy', 20);
// 2.静态成员
Person.sex = '男';
console.log(xy.sex);//undefined
console.log(Person.sex);//男
</script>
显示截图:
3.构造函数的问题
构造函数虽好用,但是它存在浪费内存的问题
<script>
function Person(name, age) {
this.name = name;
this.age = age;
this.eat = function () {
console.log('吃饭');
}
}
var xy=new Person('xy',20);
var xx=new Person('xx',20);
</script>
如上,同一个构造函数当它实例化两个或更多对象时,如果都存在类似以上eat方法这种复杂类型时,需要额外开辟内存空间存放函数,但是其实它们函数是一样的,这就造成了内存的冗余浪费。
4.构造函数原型 prototype
Javascript规定,每一个构造函数都有一个prototype属性,指向另一个对象。注意这个prototype就是一个对象,这个对象的所有属性和方法,都会被构造函数所拥有。
我们可以把哪些不变的方法,直接定义在prototype对象上,这样所有对象的实例就可以共享这些方法
简答:
问:1.原型是什么?
答:一个对象,我们也称为prototype原型对象。
问:2.原型的作用是什么?
答:共享方法
5.对象原型 __proto__
对象都会有一个属性__proto__指向构造函数的prototype原型对象,之所以我们对象可以用构造函数prototype原型对象的属性和方法,就是因为对象有__proto__原型的存在。
注意:
__proto__
对象原型和原型对象prototype是等价的__proto__
对象原型的意义就在于为对象的查找机制提供一个方向,或者说一条路线,但是它是一个非标准属性,因此实际开发中,不可以使用这个属性,它只是内部指向原型对象prototype。
如下:
<script>
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.eat = function () {
console.log('吃饭');
}
var xy = new Person('xy', 20);
xy.eat();
console.log(xy);
</script>
其中的xy对象中存在__proto__指向对象Object,其实也就是指向Person里的原型对象prototype,所以可以调用原型对象里的eat方法;
6.原型constructor构造函数
对象原型__proto__ 和构造函数prototype原型对象里面都有一个constructor属性,constructor我们称为构造函数,因为它指回构造函数本身。
console.log(xy.__proto__);
console.log(Person.prototype);
constructor主要用于记录对象引用于哪个构造函数,它可以让原型对象重新指向原来的构造函数。
例如如果我们需要声明多个函数,我们可以这样写
<script>
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype = {
eat: function () {
console.log('吃饭');
},
sleep: function () {
console.log('睡觉');
}
}
var xy = new Person('xy', 20);
xy.eat();
xy.sleep();
</script>
但是存在一个问题就是这样赋值的写法会导致将原来的原型对象给覆盖,其中的constructor指向也会改变,如下:
console.log(xy.__proto__.constructor);
console.log(Person.prototype.constructor);
所以我们可以手动添加constructor让它重新指回Person构造函数
<script>
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype = {
constructor: Person,//手动添加重新指向
eat: function () {
console.log('吃饭');
},
sleep: function () {
console.log('睡觉');
}
}
var xy = new Person('xy', 20);
xy.eat();
xy.sleep();
console.log(xy.__proto__.constructor);
console.log(Person.prototype.constructor);
</script>
显示结果:
7.构造函数、实例、原型对象三者之间的关系
以上例子举例:
1.
1.构造函数与原型对象
Person构造函数: 通过构造函数里的prototype属性指向Person的原型对象
Person原型对象:通过原型对象里的constructor属性重新指回Person构造函数。
2.构造函数与对象实例
Person构造函数: 通过new创建xy对象实例
xy对象实例: 通过对象原型__proto__指向Person的原型对象prototype,再通过原型对象的construcor指向构造函数
8.原型链
前面说了既然是对象都会有一个__proto__属性指向原型对象,那么这里的Person原型对象本质也是一个对象,那么它有没有__proto__属性呢,又指向哪个原型对象呢?
不妨打印出来看看。
<script>
function Person(name, age) {
this.name = name;
this.age = age;
}
console.log(Person.prototype.__proto__);
</script>
显示结果:
事实证明,确实存在__proto__,并且指向的原型对象的constructor属性指向了Object()构造函数。
有了进一步发现,同理,那么Object的原型对象prototype的__proto__属性呢?又会指向谁?
继续打印:
console.log(Object.prototype.__proto__);
结果为null
所以最终我们可以得到如下关系图:
从对象实例往上查找,一系列原型对象就构成了一条原型链。
9.成员查找机制
其实就是按照原型链网上查找
- 当访问一个对象的属性或者方法,首先查找对象自身有没有该属性。
- 如果没有就查找它的原型(__proto__指向的prototype)
- 如果还没有就查找原型对象的原型(Object的原型对象)
- 直到null为止
10.原型对象this指向
<script>
var thatPerson;
var thatprototype;
function Person(name, age) {
this.name = name;
this.age = age;
thatPerson = this;
}
Person.prototype.eat = function () {
thatprototype = this;
console.log(this);
}
var xy = new Person('xy', 20);
// 1.构造函数this
console.log(thatPerson);
// 2.原型对象this
xy.eat();
console.log(thatPerson === thatprototype);
</script>
显示结果:
可以知道构造函数this和原型对象this指向相同,都是实例对象。
结论:构造函数里的this和原型对象里的this都指向对象实例!
11.继承
ES6之前并没有给我们extends继承,我们可以通过构造函数+原型对象模拟实现继承,被称为组合继承,构造函数继承属性,原型对象继承方法。
1.借用构造函数继承父类型属性
核心原理:通过call()把父类型的this指向子类型的this,这样就可以实现子类型继承父类下的属性。
<script>
function Father(name, age) {
this.name = name;
this.age = age;
}
function Son(name, age) {
Father.call(this, name, age);
}
var son = new Son('xy', 20);
console.log(son);
</script>
显示如下:
通过call(this,name,age)将Father的this属性指向Son,则Son得到了Father内的this.name,this.age。
当然这里也不会影响Son自身的属性,如下的sex属性
<script>
function Father(name, age) {
this.name = name;
this.age = age;
}
function Son(name, age, sex) {
this.sex = sex;
Father.call(this, name, age);
}
var son = new Son('xy', 20, '男');
console.log(son);
</script>
2.利用原型对象继承方法
因为我们借用call方法只改变了Father内的this指向,让Son得到了Father内的属性,但是方法却得不到,因为我们为了节省内存一般将方法存到了原型对象中,我们仅仅通过构造函数无法得到方法,这里就需要用到原型对象继承方法。
<script>
function Father(name, age) {
this.name = name;
this.age = age;
}
Father.prototype.eat = function () {
console.log('吃饭');
}
function Son(name, age, sex) {
this.sex = sex;
Father.call(this, name, age);
}
//Son.prototype = Father.prototype; 1.直接赋值
Son.prototype = new Father();// 2.间接继承
Son.prototype.school = function () {
console.log('上学');
}
var son = new Son('xy', 20, '男');
var father = new Father('fa', 45);
console.log(son);
console.log(father);
</script>
如上,Father的原型对象里存了一个eat方法,Son要想得到这个方法,我们一般会有两种想法:
1. 直接得到(不可取)
也就是直接将Father的原型对象赋给Son的原型对象
Son.prototype = Father.prototype;
显示结果:
虽然我们能看到Son里确实得到了eat方法,但是Son的原型对象里再添方法的话(如school)还会影响Father原型对象相应改变,这种直接原型赋值的方法不可取,因为它会造成方法的同步改变。
2. 间接得到
间接得到其实就是通过将Son指向Father的实例对象,根据原型链知道,Father的实例对象又指向了Father的原型对象,这样一来我们就能间接得到了Father里的方法,并且不会对Father的原型对象造成影响。
Son.prototype = new Father();