JS 面向对象(创建对象、构造函数、原型继承、原型、原型链)

310 阅读10分钟

面向对象

  • 对象:万物皆对象
  • 类:对对象的细分
  • 实例:类中具体的事物
创建对象
  • 第一种方法(通过对象字面量表示法)
var Person = {
    name:'tom',
    age:22,
    getName:function(){
       alert(this.name)
    }
}
  • 第二种方法(通过new和构造函数Object()、String()等)
var People = new Object();
People.name = 'tom'
People.age = 22,
People.getName = function(){
   alert(this.name)
}
  • 第三种方法(用自定义构造函数初始化新对象)
function Person(name,age){
   this.name = "test"
   this.age =22
   this.sayName = function {
   alert(this.name)
  }
}
var myFather = new Person("test",22);
  • 第四种方法(通过 Object.create())
var o1 = Object.create({x:1,y:2})

面向对象三大特性:封装、继承、多态

封装

定义:把客观事物封装成抽象的类,隐藏属性和方法的实现细节,仅对外公开接口。

  • ES6之前,没有class概念,借由原型对象和构造函数来实现
function Cat(name,food){
   this.name = name // 公有属性
   this.food = food
}
Cat.prototype.say = function(){  // 共有方法
    console.log(thia.name + "likes" + this.food)
}
Cat.see = function(){  // 静态方法
   console.log("静态方法,无需实例化可调用")
}
var cat = new Cat('Tom','mouse')
cat.say()  // 实例共享原型属性和方法
  • ES6 class
class Cat{
   constructor(name,food){
       this.name = name 
       this.food = food
   }
   static see(){
     console.log('静态方法,无需实例化可调用')
   }
   say(){
     console.log(this.name + 'like' + this.food)
   }
}
var cat = new Cat('tom','mouse')
cat.say()
继承
复制属性式继承
// 创建父对象
var parentObj = {
	name: 'parentName',
	age: 25,
	showName:function(){
        console.log(this.name);
    }
}
// 创建需要继承的子对象
var childrenObj= {}
// 开始拷贝属性(使用for...in...循环)
for(var i in parentObj){
	childrenObj[i] = parentObj[i]
}
console.log(childrenObj); //{ name: 'parentName', age: 25, showName: [Function: showName] }
console.log(parentObj); // { name: 'parentName', age: 25, showName: [Function: showName] }
  • 重点:将父对象的函数和方法循环进行复制,复制到子对象里;
  • 缺点:如果继承过来的成员是引用类型的话,那么这个引用类型的成员在父对象和子对象之间是共享的,也就是说修改了之后, 父子对象都会受到影响。
原型继承
  • 原型式继承就是借用构造函数的原型对象实现继承,即子构造函数.prototype = 父构造函数.prototype
// 创建父构造函数
function Parent(){}
// 设置父构造函数的原型对象
Parent.prototype.age = 25;
Parent.prototype.friends = ['小名','小丽'];
Parent.prototype.showAge = function(){
    console.log(this.age);
};
// 创建子构造函数
function Child(){}
// 设置子构造器的原型对象实现继承
Child.prototype = Parent.prototype
// 因为子构造函数的原型被覆盖了, 所以现在子构造函数的原型的构造器属性已经不再指向Child,而是Parent。此时实例化Child和实例化parent的区别是不大的,所以再次创建Child是没有意义的,并且Child.prototype添加属性,也是会影响到Parent.prototype;
console.log(Child.prototype.constructor == Parent);// true
console.log(Parent.prototype.constructor == Parent);// true

// 问题就在这里!!!!
// 所以我们需要修正一下
Parent.prototype.constructor = Child;
// 上面这行代码之后, 就实现了继承
var childObj = new Child();
console.log(childObj.age);// 25
console.log(childObj.friends);// ['小名','小丽']
childObj.showAge();// 25
  • 问题:

1:只能继承父构造函数的原型对象上的成员, 不能继承父构造函数的实例对象的成员. 2:父构造函数的原型对象和子构造函数的原型对象上的成员有共享问题;

原型链继承:得到方法
// 定义父构造函数
function Parent(name,friends){
	this.name = name;
	this.friends = friends;
}
Parent.prototype.test = function(){
	console.log('原型方法', this.friends)
};
// 定义子构造函数
function Child(name,friends,age){
    this.age = '12'
}
// 将子构造函数的原型指定父函数的实例
Child.prototype = new Parent('parentName',['a','b','c']);
// 但是
console.log(Child.prototype.constructor); 
//输出:function Parent(){this.name = 'me';this.sex = ['male','female']}
// 所以,把Child的原型的构造函数修复为child
Child.prototype.constructor = Child
var childObj = new Child('childName',[3,4,'ddd'],24);//有test()

// 问题一:子实例无法向父类传值
console.log(childObj.name,childObj.friends) // parentName和["a", "b", "c"]
// 问题二:如果其中一个子类修改了父类中的引用数据类型的属性,那么就会影响其他的子类
var childObj2 = new Child('childName',[3,4],24);
childObj2.friends.push('additem')
console.log(childObj1.friends,childObj2.friends)//  ["a", "b", "c", "additem"], ["a", "b", "c", "additem"]
借用构造函数call(经典继承) : 得到属性
  • 使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类
  • 问题:child无法继承parent原型上的对象,并没有真正的实现继承(部分继承)
function Parent(xxx){this.xxx = xxx}
Parent.prototype.test = function(){};
function Child(xxx,yyy){
    Parent.call(this, xxx);
}
var child = new Child('a', 'b');  //child.xxx为'a', 但child没有test()
// 问题:
console.log(child.test);// undefined
  • 特点:创建子类实例时,可以向父类传递参数。可以实现多继承
组合式继承
  • 借用构造函数+ 原型式继承
// 创建父构造函数
// 父类属性
function Parent(name){
	this.name = name;
	this.sex = ['male','female']
}
// 父类原型方法
Parent.prototype.test = function(){
	console.log(this.name)
};
// 定义子构造函数
function Child(name,age){
	// 复制父级构造函数的属性和方法
	// 使得每一个子对象都能复制一份父对象的属性且不会相互影响
    Parent.call(this,name);//继承实例属性,第一次调用Parent()
    this.age = age
}
// 将子构造函数的原型对象指向父级实例
var parentObj = new Parent();//继承父类方法,第二次调用Parent()
Child.prototype = parentObj; //得到test()
// 将子构造函数Child原型的构造函数修复为Child
Child.prototype.constructor = Child; 
var childObj = new Child('zhangsan',15); console.log(childObj,childObj.name,childObj.sex,childObj.test)
// 输出:childObj.name:'zhangsan';childObj.sex:["male", "female"];childObj.test:一个函数

相当重要的一步:Child.prototype.constructor = Child;

1.任何一个Prototype对象都有一个constructor指针,指向它的构造函数; 2.每个实例中也会有一个constructor指针,这个指针默认调用Prototype对象的constructor属性。 结果:当替换了子类的原型之后,即 Child.prototype = new Parent()之后,Child.prototype.constructor 就指向了Parent(),Child的实例的constructor也指向了Parent(),这就出现问题了。 因为这造成了继承链的紊乱,因为Child的实例是由Child构造函数创建的,现在其constructor属性却指向了Parent,为了避免这一现象,就必须在替换prototype对象之后,为新的prototype对象加上constructor属性,使其指向原来的构造函数。

一、普通对象与函数对象

JavaScript中,万物皆对象!但对象也是有区别的。分为普通对象和函数对象,Object 、Function 是 JS 自带的函数对象。

// 普通对象
var o1 = {}; 
var o2 = new Object();
var o3 = new f1();

console.log(typeof o1); //object 
console.log(typeof o2); //object 
console.log(typeof o3); //object

// 函数对象
function f1(){}; 
var f2 = function(){};
var f3 = new Function('str','console.log(str)');

console.log(typeof Object); //function 
console.log(typeof Function); //function  

console.log(typeof f1); //function 
console.log(typeof f2); //function 
console.log(typeof f3); //function   

解析:例子中 o1 o2 o3 为普通对象,f1 f2 f3 为函数对象。

如何区分??

凡是通过 new Function() 创建的对象都是函数对象,其他的都是普通对象。

f1,f2,归根结底都是通过 new Function()的方式进行创建的。Function Object 也都是通过 New Function()创建的。

总结:凡是使用 function 关键字或 new Function() 构造函数创建的对象都是函数对象。

只有函数对象才拥有 prototype (原型)属性。

二、构造函数

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function() { 
        alert(this.name)
   } 
}
var person1 = new Person('Zaxlct', 28, 'Software Engineer');
var person2 = new Person('Mick', 23, 'Doctor');

解析:例子中 person1 和 person2 都是 Person 的实例。这两个实例都有一个 constructor (构造函数)属性,该属性(是一个指针)指向 Person。

console.log(person1.constructor == Person); //true
console.log(person2.constructor == Person); //true

总结:

  • 构造函数始终都应该以一个大写字母开头,而非构造函数则应该以一个小写字母开头
  • person1 和 person2 都是 构造函数 Person 的实例。
  • 实例的构造函数属性(constructor)指向构造函数。
通过构造函数创建一个对象?
function Person(name){
   this.name = name
}
let son = new Person('tom')
console.log(son) // Person{name:"tom"}

三、原型对象----(原型prototype)

JavaScript 中,每当定义一个对象(函数也是对象)时候,对象中都会包含一些预定义的属性。其中每个函数对象都有一个prototype 属性,这个属性指向函数的原型对象。

function Person() {}

Person.prototype.name = 'Zaxlct';
Person.prototype.age  = 28;
Person.prototype.job  = 'Software Engineer';

Person.prototype.sayName = function() {
  alert(this.name);
}
  
var person1 = new Person();

person1.sayName(); // 'Zaxlct'
var person2 = new Person();
person2.sayName(); // 'Zaxlct'

console.log(person1.sayName == person2.sayName); //true

总结:

每个对象都有内置 __proto__ 属性,但只有函数对象才有 prototype 属性

每一个JavaScript对象(null除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型"继承"属性。

什么是原型对象呢???

Person.prototype = {
   name:  'Zaxlct',
   age: 28,
   job: 'Software Engineer',
   
   sayName: function() {
     alert(this.name);
   }
}

原型对象,它就是一个普通对象。原型对象就是 Person.prototype ,简化下,把它想想成一个字母 A: var A = Person.prototype

在默认情况下,所有的原型对象都会自动获得一个 constructor(构造函数)属性,
这个属性(是一个指针)指向 prototype 属性所在的函数(Person)

简化下:A 有一个默认的 constructor 属性,这个属性是一个指针,指向 Person。即:Person.prototype.constructor == Person

四、内置属性_proto_

JS 在创建对象(不论是普通对象还是函数对象)的时候,都有一个叫做__proto__ 的内置属性,用于指向创建它的构造函数的原型对象。

对象 person1 有一个 __proto__属性,创建它的构造函数是 Person,构造函数的原型对象是 Person.prototype ,所以: person1.__proto__ == Person.prototype

Person.prototype.constructor == Person;
person1.__proto__ == Person.prototype;
person1.constructor == Person;
  • 每一个对象自身都拥有一个隐式的[[proto]]属性,该属性默认是一个指向其构造函数原型属性的指针;
  • 几乎所有函数都拥有prototype原型。

五、原型链

1、person1.__proto__ 是什么?

2、Person.__proto__ 是什么?

3、Person.prototype.__proto__ 是什么?

4、Object.__proto__ 是什么?

5、Object.prototype__proto__ 是什么?

答案: 第一题: 因为 person1.__proto__ === person1 的构造函数.prototype 因为 person1的构造函数 === Person 所以 person1.__proto__ === Person.prototype

第二题: 因为 Person.__proto__ === Person的构造函数.prototype 因为 Person的构造函数 === Function 所以 Person.__proto__ === Function.prototype

第三题: Person.prototype 是一个普通对象,我们无需关注它有哪些属性,只要记住它是一个普通对象。 因为一个普通对象的构造函数 === Object 所以 Person.prototype.__proto__ === Object.prototype

第四题,参照第二题,因为 Person 和 Object 一样都是构造函数

第五题: Object.prototype 对象也有proto属性,但它比较特殊,为 null 。因为null 处于原型链的顶端,这个只能记住。 Object.prototype.__proto__ === null

总结:

原型

Javascript规定,每一个函数都有一个prototype对象属性,指向另一个对象(原型链上面的)。

prototype(对象属性)的所有属性和方法,都会被构造函数的实例继承。这意味着,我们可以把那些不变(公用)的属性和方法,直接定义在prototype对象属性上。

prototype就是调用构造函数所创建的那个实例对象的原型(proto)。

原型链总结

原型链是一种机制,指的是 JavaScript 每个对象都有一个内置的 __proto__ 属性指向创建它的构造函数的 prototype(原型)属性。

1;每个构造函数都有一个原型对象

2;每个原型对象都有一个指向构造函数的指针

3;每个实例函数都有一个指向原型对象的指针。

4;查找方式是一层一层查找,直至顶层Object.prototype

原型链最直接的表达

创建对象(不论是普通对象还是函数对象)时,都有内置属性__proto__,指向创建它的函数对象的原型对象 prototype。

例子:

function Person(){}

var person = new Person()
console.log(person.__proto__ === Person.prototype)  // true

同样的,函数的原型对象(Person.prototype) 也有 proto 属性,它指向创建它的函数对象(Object)的prototype。

console.log(Person.prototype._proto_ === Object.prototype) // true

继续,Object.prototype对象也有__proto__属性,但它比较特殊,为null

console.log(Object.prototype.__proto__) // null

我们把这个有 proto 串起来的直到 Object.prototype.proto 为null的链叫做原型链。

person.__proto__ ==> Person.prototype.__proto__ ==> Object.prototype.__proto__ ==> null
Object.__proto__ === Function.prototype; 
Function.prototype.__proto__ === Object.prototype; 
Object.prototype.__proto__ === null;

构造函数、原型、实例三者的关系

  • 创建一个函数,该函数会自动带有一个prototype属性,指向一个对象,称为原型对象。
  • 原型对象上默认有一个属性constructor,该属性也是一个指针,指向与其相关的构造函数
  • 通过调用构造函数产生的实例,都有一个内部属性,指向了原型对象。

总结:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,实例都包含一个指向原型对象的内部指针。