JavaScript之面向对象编程(笔记一)

266 阅读17分钟

之前说到this指向,this指向的四种绑定对应四种函数的调用,说到底JS如此复杂的原因是因为函数过于强大,能改造很多形式。因为函数是个对象,原型链就比较复杂,因为函数还可以作为值被传递,所以执行环境栈就复杂了,同样的话函数具有多种调用方式 ,所以this的绑定也比较复杂,只有理解了JS的函数才算理解了JavaScript。

内容介绍

本章内容主要是基于面向对象来实现编程的一些方案,在之前JS基础到页面上的一些行为都是面向过程去做的,何为面向过程,就这一步做什么下一步做什么根据逻辑来进行实现的,面向对象就是在面向过程的基础上对它二次改造,二次改造就是面向对象实现了可扩展性,在这个对象上来进行改造,还能进行添加、删除。

对象是什么:

说到对象我们就想起构造函数,构造函数就要想起有new关键字,在构造函数中构造的实例化中有一个属性叫constructor,每一个构造函数都有一个原型对象,这个原型对象上也有它自己的像原型链、constructor等等的一些概念。

创建对象的几种方式

对象的字面量,工厂模式,构造函数模式,原型链模式,组合模式

对象是什么

  1. 对象是单个实物的抽象,比如水杯、人、电脑都可以是对象,当实物被抽象出来变成一个对象后,实物之间的关系就变成了对象之间的关系
  2. 对象是一个容器,封装了对应的属性和方法 比如一个杯子,他的高度,容量这是它的属性,杯子能喝水,这就是它的方法。

属性是对象的状态,方法是对象的行为(就是我们要完成的任务)

面试向对象编程式目前主流的一个编程模式,那么它将我们真实事件的复杂关系抽象成了一个一个的对象,然后由这个对象之间的分工合作完成对真实事件的一个模拟,就是说我们在我们的生活中一些真实存在的这些模拟的场景或者一些模拟的对象然后在我们代码中,通过代码模拟出真实的世界来,这实际上就是面向对象的一个过程。每一个对象都有一个公共中心,公共中心就是自己做自己的事情。比如有一个对象具有明确的分工可以完成接收信息,处理数据,发出信息等任务,对象另外还可以是复用的,我创建出来一个对象,别人这里面的功能如果给我的这个对象是一样的,那么可以去复用我的对象,所以说通过继承机制,可以去定制对象。因此面向对象编程有灵活代码、可复用,高度模块化等特点。

面向对编程(OOP)优点:

具有灵活、代码可复用性,高度模块化等特点

构造函数

面向对象的第一步就是如何来生成对象,如果想生成一个对象,我们需要一个模板,那么这个模板里面表示了这一类实物的共同特征(比如:所有实物都有年龄,名字等共同特征)

典型的面向对象编程语言(比如 C++ 和 Java),都有“类”(class)这个概念。所谓“类”就是对象的模板,对象就是“类”的实例。但是,JavaScript 语言的对象体系,不是基于“类”的,而是基于构造函数(constructor)和原型链(prototype)。

构造函数就是作为了JS中的一个模板,我们想要创建一个类呢就要去通过new这个构造函数来实例化出来这个类

function Dog(name,age){
    //name和age就是当前实例化对象的属性
	this.name=name;
	this.age=age;
}
var dong= new Dog("小黄",3)
console.log(dong.name)

创建一个Dog类,这个Dog里面有它自己的名字好和年龄,在里面让Dog赋值 this.name=name;,当直接Dog()这样去调用的时候就相当于函数的调用, 如果前面加了一个关键字new,那么相当于一个构造函数,内部this指向了实例化对象dong,接下来dong赋值给name

Dog是构造函数,为了与普通函数区分,构造函数的名字第一个字母通常大写

构造函数有两个特点:

  1. 函数体内使用了this关键字,就是当前实例化对象的属性
  2. 生成对象必须使用new关键字实例化当前对象

根据需要,构造函数可以接受参数

function Dog (name){
    this.name = name;
}
var d1 = new Dog('小黄');
console.log(d1.name);//小黄

如果忘记使用new操作符,则this将代表全局对象window

function Dog(name,age){
    thiss.name=name;
    this.age=age;
}
var dong=Dog("小黄"3)   //undefined
console.log(dong);

如果忘记写new了,就相当于函数调用。这里的返回值返回了当前函数调用的返回值,这里没有返回值,就是undefined,如果忘记写关键字new也不会报错,这个时候最好给函数加上严格模式use strict

function Dog(name){
    'use strict';
    this.name = name;
}
var dong = Dog('阿黄');
console.log(dong)

上述代码use strict命令保证了该函数在严格模式下运行。由于严格模式中,函数内部的this不能指向全局对象,默认等于undefined,导致不加new调用会报错(JavaScript不允许对undefined添加属性)。

那如果就是忘记写了,那么有什么解决办法,就是在声明这么一个函数,有没有解决办法能让它给我生成这种对应的实例呢?这个时候我们就通过JS中有个一个instanceof运算符来解决

instanceof用法

instanceof这个运算符运行时,它能表示我们当前对象是否是特定类实例,如果是返回true,不是就返回false

a instanceof b  //这句话就是说,a是不是b的一个实例,如果是就返回true,如果不是就返回false

通常我们都用instanceof来检查是不是它的一个引用类型

function Dog(name){
   if(!(this instanceof Dog)){
	   return new Dog(name);
   }else{
    this.name = name;  
   }
    
}
var dong = Dog('阿黄');
console.log(dong)

上述代码中的构造函数,不管加不加new命令,都会得到同样的结果

代码解释:

创建一个Dog里面传入一个变量name,在下面如果没写new,就在内部做一层判断,this现在直接指向window,就是说这个this不是它的一个实例(!(this instanceof Dog)),接下来我在这里面重新的去new一个Dog,把name给它传进去,如果是它的一个实例就走else(this.name = name;),就是看一下当前在去实例化这个函数的时候有没有去加new这个关键字,如果加了new就会走(this.name = name;),如果没加new就去走 return new Dog(name);,因为没加new内部的this就指向window,window然后来了个Dog这个返回值this instanceof Dog为false,意思就是window不是Dog的一个实例,所以再取一下非,就为true,为true的话就意味着走进这个代码 return new Dog(name);,走进来就再去实例化一下 new Dog(name);,实例化的结果给了dong,结果不管怎么去调用结果都是dong的实例化。

还有一种写法

function Dog(name){
   if(this instanceof Dog){
	  this.name = name;
   }else{
	 return new Dog(name);
   }
}
var dong = Dog('小黄');
console.log(dong)

new命令内部原理

在构造一个对象的时候,如果我们想要创建一个对象,声明构造函数之后必须使用new关键字来进行实例化对象,如果不使用关键字new来实例化对象的时候,我们只能通过instanceof这个运算符来进行从操作,基于这个方案,我们来研究一下new这个命令的原理

new命令的原理

在之前构造一个对象的时候我们能看到如果我们想要创建一个对象,我们声明构造函数之后必须使用new关键字来进行实例化对象,如果我们不使用new关键字来实例化对象的话,那么我们只能通过instanceof运算符来进行操作,那么我们排除使用instanceof来进行操作的方案,我们来研究一下new这个命令的原理

我们通过new命令来操作的时候,这个时候它发生了四步我们通过一个例子来解释

function Person(){
    
}
var p1=new Person();

p1是实例化对象,Person是构造函数对象,实例化对象和构造函数对象有一个关系,构造函数作为一个模板实例化出p1对象。 首先通过new关键字来构造这个Person()函数之后,接下来创建了一个空对象new Person();,然后这个空对象作为要返回的实例给了p1,给了p1之后函数就到了Person(){}内部中,这个时候给new Person()传入一个名字小太阳new Person("小太阳")Person()里面就有一个属性namePerson(name),这个时候就要当前的实例this赋值,意思就是把当前的实例对象赋值给了当前的thisthis.name=name,

  1. 创建一个空对象作为一个将要返回的对象实例
  2. 将这个空的对象原型对象,指向了构造函数的prototype属性对象
  3. 将这个实例对象赋值给函数内部的this关键字
  4. 执行构造函数体内的代码

constructor属性

每个对象在创建时都自动拥有一个构造函数属性contructor,其中包含了一个指向其构造函数的引用。而这个constructor属性实际上继承自原型对象,而constructor也是原型对象唯一的自有属性

function Person(name){
	this.name=name
}
var p1=new Person("小太阳")
console.log(p1)
console.log(p1.constructor===Person)

当前里面有一个构造函数function Person(name){this.name=name},new关键字实例化出来new Person("小太阳")的这个对象中如果打印一下p1,有两个属性 这时候看不见constructor,constructor是通过继承关系继承下来的,当前的Preson对象虽然已经出来了,Preson也有父类,它的父类就是__proto__,这下面就有它的constructor属性

我们如果p1实例来调用constructor的时候,相当于继承关系继承下来的,那它是继承了它的原型对象__proto__

constructor继承自原型对象,其中指向了构造函数的引用

console.log(p1.constructor===Person)

constructor是我们原型对象唯一的自由属性

使用构造函数创建对象的利与弊

函数中的return语句用来返回函数调用后的返回值,而new构造函数的返回值有点特殊。

如果构造函数使用return语句但没有指定返回值,或者返回值是一个原始值,那么这时将忽略返回值,同时使用这个新对象作为调用结果

function Fn(){
    this.a = 2;
    return;
}
var test = new Fn();
console.log(test);//{a:2}

如果构造函数显式地使用return语句返回一个对象,那么调用表达式的值就是这个对象

var obj = {a:1};
function fn(){
    this.a = 2;
    return obj;
}
var test = new fn();
console.log(test);//{a:1}

使用构造函数的好处在于所有用同一个构造函数创建的对象都具有同样的属性和方法

function Person(name){
    this.name = name;
    this.sayName = function(){
        console.log(this.name);
    }
}
var p1 = new Person('Tom');
var p2 = new Person('Jack');

构造函数允许给对象来配置同样的属性,但是构造函数并没有消除代码冗余。使用构造函数的主要问题是每个方法都要在每个实例上重新创建一遍。在上面的例子中,每一个对象都有自己的sayName()方法。这也意味着如果有100个对象实例,就有100个函数做相同的事情,只是使用的数据不同。

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.sayName === p2.sayName);//false

上面代码中,p1和p2是用一个构造函数的两个实例,他们具有sayName方法。由于sayName方法是生成在每个实例对象上面,所以两个实例就生成了两次。也就是说,每创建一个实例,就会新建一个sayName方法。这既没有必要,又浪费系统资源,因此所有sayName方法都是同样的行为,完全应该共享。

这个问题的解决方法。就是JavaScript的原型对象(prototype)

原型对象

用一个例子来说明三者之间的关系

function Foo(){}
var f1=new Foo();
var f2=new Foo();

通过构造函数使用关键字new来操作创建出来的f1对象,就作为我们的一个实例对象,每一个原型对象中都有一个属性可以创建一个实例对象,也可以创建多个实例对象,f1和f2是两个不同的实例对象,因为它们的内存地址是不一样的.

原型对象:是构造函数的prototype,(比如:Foo.prototype)。

实例对象:f1就是实例对象,每一个原型对象中都有一个__proto__。每个实例对象都有一个constructor属性,这个constructor是通过继承关系继承下来的。它指向了当前的构造函数Foo

构造函数对象:用来初始化新创建对象的函数:Foo就是构造函数,自动给构造函数赋予一个属性叫protoptype原型属性,该属性指向了实例对象的原型对象

使用原型对象来做它共有的方法

function Foo(){};
Foo.prototype.showName=function(){//原型对象给它赋予一个属性showName
	console.log("小太阳");
}
var f1=new Foo();
var f2=new Foo();
console.log(f1.showName());   //不同的实例调用了相同的方法做了相同的事情
console.log(f2.showName());

原型对象、实例对象、构造函数对象三者之间的关系

pototype属性的作用

JavaScript继承机制的设计思想就是,原型对象的所有属性和方法,都能被实例对象共享。也就是说,如果属性和方法定义在原型上,那么所有实例对象就能共享,不仅节省了内存,还体现了实例对象之间的联系。

function Foo(){};
Foo.prototype.name="小太阳";
var f1 = new Foo();
console.log(f1.name)   //小太阳被继承下来了
console.log(f1)      //不存在name属性,name属性在原型对象__proto__上

这就是JS继承,当前的f1实例继承了原型对象

如何为我们的对象指定原型,js规定每个函数Foo都有一个prototype属性,指向了一个对象。这个就是prototype的作用。name属性是放置在我们原型属性上的一个属性值,如果我们实例化两个这样的对象,看看这两个值:

function Foo(){};
Foo.prototype.name="小太阳";
var f1 = new Foo();
var f2 = new Foo();
console.log(f1.name)   //小太阳
console.log(f2.name)    //小太阳

修改当前的name

function Foo(){};
Foo.prototype.name="小太阳";
var f1 = new Foo();
var f2 = new Foo();
console.log(f1.name)   //小太阳
console.log(f2.name)    //小太阳
Foo.prototype.name="小胖子";     //两个实例都会被修改,小胖子
console.log(f1.name)   //小胖子
console.log(f2.name)    //小胖子

pototype总结

JS继承机制:通过原型对象实现继承,通过prototype来实现继承机制。每一个函数中都有一个prototype属性

原型对象的作用:就是定义了所有的实例对象共享的属性和方法。

原型链

JS规定所有的对象都有它自己的原型对象,一方面我们认可一个对象都可以充当其他对象的一个原型,另一方面由于原型对象也是对象那么它也有自己的原型,因此就会形成自己的原型链。

根据原型链查找,如果一层一层往上查找,所有对象的原型最终都可以查找到Object.prototype,也就是说最终找到Object这个构造函数的prototype。

所有对象都继承了Object.prototype上的属性和方法。这也就是为什么我们在之前用数组它里面有toString(),用数值也有toString()将我们一个数组转换成一个字符串,是因为他们所有的方法都定制在当前Object.prototype上

function Person(name){
	this.name=name;
}
Person.prototype.showName=function(){
	console.log(this.name)
}
var p1= new Person("小胖子");
var p2=new Person("小太阳");
p2.showName();

Person可以定制它的名字把名字传入进去Person(name),构造一个实例对象var p1= new Person("小胖子");,p1是实例的原型指向Person的prototype,如果给Person定制一个方法,希望这个方法共享出来,p1和p2共享于Person定制的方法,在原型对象上给它定制一个showName的这个么一个方法,这样的话在里面打印一下this.name。这个this指向Person这个构造函数,这个原型上有一个name属性

读取属性和方法的规则:JS引擎会现寻找对象本身的属性和方法,如果找不到就到它的原型对象去找,如果还是找不到,就到原型的原型去找,如果直到最顶层的Object.prototype还是找不到,就会返回undefined。如果对象和原型都定制了同名的属性,那么优先读取自身的属性,也就是覆盖了。

修改原型对象后constructor属性的注意点

原型对象默认只会取得constructor属性,指向该原型对象对应的构造函数。至于其他方法,则是从Object继承来的

function Foo(){};
console.log(Foo.prototype.constructor === Foo);//true

由于实例对象可以继承原型对象的属性,所以实例对象也拥有constructor属性,同样指向原型对象对应的构造函数

function Foo(){};
var f1 = new Foo();
console.log(f1.constructor === Foo);//true
console.log(f1.constuctor === Foo.prototype.constructor);//false
f1.hasOwnProperty('constructor');//false

上面代码中,f1是构造函数Foo的实例对象,但是f1自身没有constructor属性,该属性其实是读取原型链上面的Foo.prototype.constructor属性

constructor属性的作用是,可以得知某个实例对象,到底是哪一个构造函数产生的。

function Foo(){};
var f1 = new Foo();
console.log(f1.constructor === Foo);//true
console.log(f1.constructor === Array);//false

constructor属性表示原型对象与构造函数之间的关联关系,如果修改了原型对象,一般会同时修改constructor属性,防止引用的时候出错

举个例子:

function Person(name){
    this.name = name;
}
console.log(Person.prototype.constructor === Person);//true
//修改原型对象
Person.prototype = {
    fn:function(){
    }
};
console.log(Person.prototype.constructor === Person);//false
console.log(Person.prototype.constructor === Object);//true

所以,修改原型对象时,一般要同时修改constructor属性的指向

function Person(name){
    this.name = name;
}
console.log(Person.prototype.constructor === Person);//true
//修改原型对象
Person.prototype = {
    constructor:Person,
    fn:function(){
        console.log(this.name);
    }
};
var p1 = new Person('阿黄');
console.log(p1 instanceof Person);//true
console.log(Person.constructor == Person);//true
console.log(Person.constructor === Object);//false

一旦我们修改了构造函数的原型对象,为防止引用出现问题,同时也要修改原型对象的constructor属性

constructor属性表示原型对象和构造函数之间的关联关系

__proto__

实例对象内部包含一个__proto__属性,指向该实例对象对应的原型对象

function Foo(){};
var f1 = new Foo;
console.log(f1.__proto__ === Foo.prototype);//true

总结

构造函数、原型对象和实例对象之间的关系是实例对象和构造函数之间没有直接联系

function Foo(){}
var f1=new Foo();

实例对象通过new这个构造函数给new出来的,在这里面构造函数代表的Foo。实例对象指的f1。Foo和f1没有直接的关系。我们通过new Foo实例化出来的f1的,关键是这个原型对象跟构造函数有关系

原型对象和实例对象的关系

实例中的.__proto__指向了当前的原型对象

function Foo(){}
var f1=new Foo();
console.log(Foo.prototype === f1.__proto__);    //true

原型对象和构造函数的关系

原型对象中的constructor属性指向构造函数

function Foo(){}
var f1=new Foo();
console.log(Foo.prototype.constructor === Foo);    //true

实例对象和构造函数 它们之间是一个间接关系,是实例对象可以继承原型对象的constructor属性

function Foo(){}
var f1=new Foo();
console.log(f1..constructor === Foo);    //true

f1constructor继承属性等于当前的构造函数

上述代码执行以后,如果重置原型对象,则会打破它们三个的关系

function Foo(){};
var f1 = new Foo;
console.log(Foo.prototype === f1.__proto__);//true
console.log(Foo.prototype.constructor === Foo);//true
Foo.prototype = {};    //修改了当前原型对象瞬间立马修改当前原型对象的constructor属性,指向与当前这个构造哈函数
console.log(Foo.prototype === f1.__proto__);//false
console.log(Foo.prototype.constructor === Foo);//false

所以,代码顺序很重要 注意:修改了当前原型对象瞬间立马修改当前原型对象的constructor属性,指向与当前这个构造哈函数