# JS面对对象基础
开篇我们先明确几个概念:
**构造函数:**函数中的一种,通过关键字new可以创建其实例。为了便于区分,通常首字母大写;
原型对象:一种特殊的对象,构造函数创建时即自动生成;与构造函数形成一一对应,如同人和影子般的关系;
实例:通过构造函数new实例化出来的对象;
# 创建对象的方式
# 1】new 操作符 + Object 创建对象
var person = new Object();
person.name = "lisi";
person.age = 21;
person.family = ["lida","lier","wangwu"];
person.say = function(){
alert(this.name);
}
这种方式直接使用Object类对象来创建实例,会产生很多重复代码。
# 2】字面式创建对象
var person = {
name: "lisi"
age: 21,
family: ["lida","lier","wangwu"],
say: function(){
alert(this.name);
}
};
使用js对象字面量创建实例对象,同理也会产生很多重复代码。
# 3】工厂模式
在上面 new Object类对象的方式 和 字面式的方式 的基础上诞生了工厂模式,可以创建多个类似对象,减少重复代码。但是无法识别对象类型功能。(因为创建出来的实例对象都是基于Object类对象直接创建的,原型链上只有Object对象)
function createPerson(name,age,family){
var o = new Object()
o.name = name
o.age = age
o.family = family
o.say = function(){
alert(this.name);
}
return o
}
var person1 = createPerson("lisi",21,["lida","lier","wangwu"]);
//instanceof无法判断它是谁的实例,只能判断他是对象,构造函数都可以判断出
var person2 = createPerson("wangwu",18,["lida","lier","lisi"]);
console.log(person1 instanceof Object); //true 只能判断是原型链上有Object
//对象字面量工厂模式
function createPerson(name,age,family){
var o = {}
o.name = name
o.age = age
o.family = family
o.say = function(){
alert(this.name);
}
return o
}
# 4】构造函数模式
构造函数的作用是给实例添加各种属性,形成具有自己独有属性的实例。
function Person(name,age,family){
this.name = name
this.age = age
this.family = family
this.say = function (){
alert(this.name)
}
}
var person1 = new Person("lisi",21,["lida","lier","wangwu"]);
var person2 = new Person("lisi",21,["lida","lier","lisi"]);
console.log(person1 instanceof Object); //true
console.log(person1 instanceof Person); //true
console.log(person2 instanceof Object); //true
console.log(person2 instanceof Person); //true
console.log(person1.__proto__)//Person {}
//指向创建person1时的Person原型对象,这个原型对象上有个constructor属性
console.log(person1.__proto__.constructor);//[Function: Person]
console.log(person1.__proto__.constructor.prototype)//Person {}
//Person构造函数里面又有一个prototype属性指向Person的原型对象
//此时Person构造函数和Person原型对象就形成了一个环状结构
console.log(person1.constructor);//[Function: Person]
//访问person1实例对象上的属性,会先去person1的本身中取找属性,再去原型对象上找属性。
对比工厂模式有以下不同之处:
1、没有显式地创建对象(使用的是函数对象)
2、直接将属性和方法赋给了 this 对象
3、没有 return 语句(因为使用了new来创造实例对象,并不是直接调用构造函数)
以此方法调用构造函数步骤:(new的过程)
1、创建一个新对象
2、将构造函数的作用域赋给新对象(将this指向这个新对象)
3、执行构造函数代码(为这个新对象添加属性)
4、返回新对象 (Person原型对象的指针赋给实例对象person)
可以看出,构造函数知道自己从哪里来(通过 instanceof
可以看出其既是Object的实例,又是Person的实例)
构造函数也有其缺陷,每个实例都包含不同的Function实例( 构造函数内的方法在做同一件事,但是实例化后却产生了不同的对象,方法是函数 ,函数也是对象,构造函数里面的所有东西都会在new的时候被调用执行,创建新的对象地址去保存里面的属性和方法)
因此产生了原型模式。
# 5】原型模式
- 每个函数都有prototype属性,指向该函数原型对象(可以不写默认有)
- 每个原型对象都有constructor属性,这是一个指向prototype属性所在函数的指针(可以不写默认有)
function Person() {
}
Person.prototype.name = 'lisi'
Person.prototype.age = 21
Person.prototype.family = ["lida", "lier", "wangwu"]
Person.prototype.say = function () {
alert(this.name);
};
console.log(Person.prototype);
//Person {
// name: 'lisi',
// age: 21,
// family: [ 'lida', 'lier', 'wangwu' ],
// say: [Function]
//}
console.log(Person.prototype.constructor);
//[Function: Person]
console.log(Person);
//[Function: Person]
//我们也可以看出来对象的构造函数和原型之间形成一个环
//我们使用new关键字来创建实例对象,就可以使用实例对象来直接访问原型上的属性
var person1 = new Person()
console.log(person1.name, person1.age, person1.family, person1.say);//访问原型对象里的属性
//lisi 21 [ 'lida', 'lier', 'wangwu' ] [Function]
person1.name='zhangsan'//此时我们通过实例给构造器中添加属性name='zhangsan'
console.log(person1.name);//zhangsan
//为什么我们会出现这种情况呢?
//当构造函数进行new实例化的时候,实例对象会对构造函数中的这些属性拷贝出一份副本:
//1.当实例调用属性name的的时候,会先从这个拷贝的属性副本中搜索name属性,如果构造函数中没有name属性,就去向原型对象中搜索name属性,
//2.如果原型对象中没有name属性,就会打印出undefined
原型模式的好处是所有对象实例共享它的属性和方法(即所谓的共有属性),此外还可以如代码第16,17行那样设置实例自己的属性(方法)(即所谓的私有属性),可以覆盖原型对象上的同名属性(方法)。
# 6】混合模式(构造函数模式+原型模式)
构造函数模式用于定义实例属性,原型模式用于定义方法和共享的属性
- 规定:
- 构造函数里面都只定义私有的属性和方法;
- 原型对象上面定义共享的属性和方法;
- 构造函数外直接给对象赋值和方法就是静态属性和方法
function sub(name) {//构造函数
this.prototype = sub.prototype//每个函数都有prototype属性,指向该函数原型对象(可以不写默认有)
this.name = name
this.say = function () {
console.log('constructor')
}
}
sub.prototype = {//原型对象
constructor: sub,//每个原型对象都有constructor属性,这是一个指向prototype属性所在函数的指针(可以不写默认有)
age: 12,
hey: function () {
console.log('prototype')
}
}
sub.say = function () {//静态方法:可直接使用类对象访问,实例访问不了
console.log('static')
}
sub.address='chengdu'//静态属性:可直接使用类对象访问,实例访问不了
let person = new sub('李四')
//访问实例对象的属性,依然遵循一个原则:先去创建实例时拷贝的构造函数副本中寻找属性,再到原型对象中寻找属性
console.log(person.name, person.age)//李四 12
person.say()//constructor
person.hey()//prototype
sub.say()//static 类对象直接访问而实例访问不了
console.log(sub.address);//chengdu 类对象直接访问而实例访问不了
可以看出,混合模式共享着对相同方法的引用,又保证了每个实例有自己的私有属性。最大限度的节省了内存。
因此,当多个实例需要使用同一个属性或方法时,我们应该将该方法放于原型对象上,从而避免相同属性或方法多次独立存在于多个对象导致内存浪费。
高程中还提到了动态原型模式,寄生构造函数模式,稳妥构造函数模式。
# 总结创建实例对象的过程:
new实例化的过程:
- 创建一个新对象
- 将构造函数的作用域赋给新对象(将this指向这个新对象)
- 执行构造函数代码(为这个新对象添加属性,也就是说的构造函数中的属性拷贝成副本,将其赋给这个新对象)
- 返回新对象 (原型对象的指针赋给这个实例对象)
程序执行时,会自动生成一个原型对象(prototype)与之相关联。同时给该构造函数自动添加一个属性:prototype,该属性为指针,指向原型对象。同时,也给该原型对象添加一个属性:constructor,该属性也为指针,指向与其对应的构造函数。此时,原型对象中只会包含些默认的方法和属性。
new实例化完成后,所有实例均会与原型对象形成多对一的隐性关联关系。所有实例会共享原型对象的属性和方法,当然也包括constructor。当原型对象被添加一个属性或者方法后,均会被所有实例共享,即可以通过任意一个实例进行访问。如果原型对象的属性或方法与实例的属性或方法名称一致,则实例自身的属性或方法优先级高于原型对象上的。
**总结:**构造函数中的属性和方法仅为声明和定义,一旦实例化工作完成后,实例对象自身的属性和方法与构造函数将不在存在关联关系。原型对象与实例对象形成“备胎”关系,当通过对象访问属性或方法时,程序会优先搜索对象本身的属性或方法,不存在才会访问原型对象的方法或者属性。