面向对象编程(上)

206 阅读25分钟

一、对象是什么

面向对象编程(Object oriented Programming,缩写为OOP)是目前主流的编程范式。

特点:灵活、代码可复用、高度模块化等

【1】对象是单个实物的抽象

一本书、一辆汽车、一个人都可以是对象,一个数据库、一张网页、一个与远程服务器的连接也可以是对象。当实物被抽象成对象,实物之间的关系就变成了对象之间的关系,从而就可以模拟现实情况,针对对象进行编程。

【2】对象是一个容器,封装了属性(property)和方法(method)

属性是对象的状态,方法是对象的行为(完成某种任务)。比如,我们可以把人抽象为person记录具体是那一种动物,使用“方法”表示人的某种行为(吃饭、喝水、休息等等)。

二、构造函数

面向对象编程的第一步,就是要生成对象:前面说过,对象是是单个实物的抽象。通常需要一个模板,表示某一类实物的共同特征,然后对象根据这个模板生成。

在一些后端(java、c++)语言当中 类(class)就是对象的模板,但是js不是基于类的,而是基于构造函数(constructor)和原型链(prototype)。

Js语言使用构造函数(constructor)作为对象的模板。所谓”构造函数”,就是专门用来生成实例对象的函数。它就是对象的模板,描述实例对象的基本结构。一个构造函数,可以生成多个实例对象,这些实例对象都有相同的结构。

构造函数就是一个普通的函数,但是有自己的特征和用法。

1、构造函数的特点:

1、函数体内使用this关键字,代表了所要生成的对象实例

2、生成对象,必须使用new关键字实例化

//Cat是构造函数,为了与普通函数区分,构造函数的名字的第一个字母通常都是大写  
function Cat(name,age){
            this.name = name;
            this.age = age;
        }
        var cat = new Cat( '咪咪',3)

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

function Cat(name,age){
            this.name = name;
            this.age = age;
        }
        var cat =  Cat( '咪咪',3)
//Uncaught TypeError: Cannot read property 'name' of undefined
console.log(d1.name);

上述代码,忘记使用new命令,其实是导致d1编程了undefined,而name属性变成了全局变量。因此,应该非常小心,避免不使用new命令、直接调用构造函数

为了保证构造函数必须与new命令一起使用,一个解决办法是,构造函数内部使用严格模式,即第一行加上use strict。这样的话,一旦忘了使用new命令,直接调用构造函数就会报错。

function Cat(name){
    'use strict';
    this.name = name;
}
var d1 = Cat('咪咪');

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

2、instanceof

该运算符运行时指出对象是否是特定类的一个实例

另一个解决办法,构造函数内部判断是否使用了new命令,如果发现没有使用,则直接返回一个实例对象

instanceof操作符可以用来鉴别对象的类型

             function Cat(name, age) {
            //this instanceof Cat:判断this是Cat实例化出来的对象
            //!(this instanceof Cat):表示如果this不是指的Cat的实例化对象
            if (!(this instanceof Cat)) {
                return new Cat(name, age);
            } else {
                this.name = name;
                this.age = age;
            }

        }
        var cat = Cat('咪咪', 3);
        console.log(cat);//Cat {name: "咪咪", age: 3}

3、new命令

new命令的原理:

  1. 创建一个空对象,作为将要返回的对象实例

  2. 将这个空对象的原型,指向了构造函数的prototype(原型链)属性

  3. 将这个空对象赋值给函数内部的this关键字

  4. 开始执行构造函数内部的代码

    function Person(name){ this.name = name; } var p1 = new Person('小米'); console.log(p1.proto === Person.prototype);

在构造函数内部,this指的是一个新生成的空对象,所有针对this的操作,都会发生在这个空对象上。构造函数之所以叫“构造函数”,就是说这个函数的目的,就是操作一个空对象(即this对象),将其“构造”为需要的样子。

4、constructor属性

每个对象在创建时都会自动拥有一个构造函数属性constructor,constructor属性实际上继承自原型对象,而constructor也是原型对象唯一的自有属性

  function Person(name){
          this.name = name;
      }
      var p1 = new Person('小米');
      console.log(p1.constructor === Person);

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

优点:构造函数允许给对象来配置同样的属性

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

解决办法:使用原型对象(prototype)。让所有相同的方法被完全共享出来。

三、原型对象

1、原型对象、实例对象和构造函数的三角关系

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

构造函数

用来初始化新创建的对象的函数是构造函数。在例子中,Foo函数是构造函数,他有一个属性prototype

实例对象

通过构造函数的new操作创建的对象是实例对象,又通常被称为对象实例。可以用一个构造函数,构造多个实例对象。下面的f1f2就是实例对象。每一个实例对象中都有一个—proto—,每个实例对象有一个constructor属性,是通过继承关系继承下来的

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

原型对象

通过构造函数的new操作创建实例对象后,会自动为构造函数创建prototype属性,该属性指向实例对象的原型对象。通过同一个构造函数实例化的多个对象具有相同的原型对象。这个的例子中,Foo.prototype是原型对象

function Foo(){};
Foo.prototype.showName = function(){
    console.log('mjj')
};
var f1 = new Foo;
var f2 = new Foo;
console. log(f1.showName);//mjj
console.log(f2.showName);//mjj

2、原型链

1、js规定,所有的对象都有自己的原型对象。

2、原型链:对象的原型 => 原型的原型 => 原型的原型的原型 ====> null。

3、根据原型链查找,如果一层一层往上查找,所有对象的原型最终都可以找到object.prototype,object构造函数的prototype。

4、所有的对象都继承了obj.prototype上的属性和方法

5、toString()

6、读取属性和方法的规则:js引擎会先寻找对象本身的属性和方法,如果找不到,就到他原型对象上去找,如果一直到最顶层object.prototype上还是找不到,就会返回undefind。

所有对象都有自己的原型对象(prototype)。一方面,任何一个对象,都可以充当其它对象的原型;另一方面,由于原型对象也是对象,所以它也有自己的原型。因此,就会形成一个原型链(prototype chain):对象的原型,再到原型的原型……

如果一层层的往上寻找,所有对象的原型最终都可以寻找到Object.prototype,即Object构造函数的prototype属性。也就是说,所有对象都继承了Object.prototype的属性。这就是所有对象都有valueoftoString方法的原因,因为这是从Object.prototype继承的。

那么,Object.prototype对象有没有它的原型呢?回答是Object.prototype的原型是nullnull没有任何属性和方法,也没有自己的原型。因此,原型链的尽头就是null

Object.getPrototypeOf(Object.prototype);//null

上面代码表示,Object.prototype对象的原型是null,由于null没有任何属性,所以原型链到此为止。Object.getPrototypeOf方法返回参数对象的原型,具体介绍在对象的方法这节课中。

读取对象的某个属性时,JavaScript 引擎先寻找对象本身的属性,如果找不到,就到它的原型去找,如果还是找不到,就到原型的原型去找。如果直到最顶层的Object.prototype还是找不到,则返回undefined。如果对象自身和它的原型,都定义了一个同名属性,那么优先读取对象自身的属性,这叫做“覆盖”(overriding)

注意,一级级向上,在整个原型链上寻找某个属性,对性能是有影响的。所寻找的属性在越上层的原型对象,对性能的影响越大。如果寻找某个不存在的属性,将会遍历整个原型链。

举例来说,如果让构造函数prototype属性指向一个数组,就意味着实例对象可以调用数组方法

var MyArray = function () {};
MyArray.prototype = Array.prototype;
MyArray.prototype.constructor = MyArray;
var mine = new MyArray();
mine.push(1, 2, 3);
mine.length // 3
mine instanceof Array // true

对象有constructor和——proto——的属性;函数有prototype原型链的属性,同时函数也可以当成一个对象,所以函数同时拥有这三种属性。

3、prototype属性的作用

js继承机制:通过原型对象,实现继承

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

每个函数都有一个prototype属性,指向了一个对象

function fn(){};
//函数fn默认具有prototype属性,指向了一个对象
console.log(typeof fn.prototype);//"Object"

对于普通函数来说,该属性基本没用。但是对于构造函数来说,生成实例的时候,该属性会自动成为实例对象的原型。

function  Person(name){
    this.name = name;
}
Person.prototype.age = 18;
var p1 = new Person('大王');
var p2 = new Person('二王');
console.log(p1.age);//18
console.log(p2.age);//18

上面代码中,构造函数Personprototype属性,就是实例对象p1p2的原型对象。原型对象上添加一个age属性,结果,实例对象都共享了该属性。

原型对象的属性不是实例对象自身的属性。只要修改原型对象,变动就立刻会体现在所有实例对象上

Person.prototype.age = 40;
console.log(p1.age);//40
console.log(p2.age);//40

上面代码中,原型对象的age属性的值变为40,两个实例对象的age属性立刻跟着变了。这是因为实例对象其实没有age属性,都是读取原型对象的age属性。也就是说,当实例对象本身没有某个属性或方法的时候,它会到原型对象去寻找该属性或方法。这就是原型对象的特殊之处。

如果实例对象就有某个属性或方法,它就不会再去原型对象寻找这个属性和方法

p1.age = 35;
console.log(p1.age);//35
console.log(p2.age);//40
console.log(Person.prototype.age) //40

上面代码中,实例对象p1age属性改为35,就使得它不再去原型对象读取age属性,后者的值依然为40

总结

原型对象的作用,就是定义所有实例对象共享的属性和方法。这也是它被称为原型对象的原因,而实例对象可以视作从原型对象衍生出来的子对象

Person.prototype.sayAge = function(){
    console.log('My age is'+ this.age);
}

上面代码中,Person.prototype对象上面定义了一个sayAge方法,这个方法将可以在所有Person实例对象上面调用。

4、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);//true
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

——proto——

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

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

5、总结

        //原型对象、实例对象、构造函数三者之间的关系
        //foo.prototype为原型对象、f1为实例对象、foo为构造函数
        function foo(){};
        var f1 = new foo();
        //1、原型对象和实例对象的关系
        console.log(foo.prototype === f1.__proto__);//true

        //2、原型对象和构造函数的关系
        console.log(foo.prototype.constructor === foo);//true

        //3、实例对象和构造函数的关系 
        //间接关系实例对象可以继承原型对象的constructor属性
        console.log(f1.constructor === foo);//true

有一种情况可以打破三者之间的关系;(所以代码的顺序很重要)

Foo.prototype = {};
console.log(Foo.prototype === f1.__proto__);//false
console.log(Foo.prototype.constructor === Foo);//false

四、创建对象的5种模式

1、对象字面量

           //(1) new构造函数
            var obj = new Object();
            obj.name = 'mjj';
            console.log(obj);//{name: "mjj"}

            //(2) 对象字面量(语法糖)
            var person = {
                name:'小猿',
                age:20,
            }
            console.log(person);//{name: "小猿", age: 20}
            console.log(person.constructor);//ƒ Object() { [native code] }

            //(3)Object.create();(括号当中是一个对象,把括号当中的对象,当做生成的新对象的原型)
            //从一个实例对象当中生成另一个实例对象
            //Object.create()中的参数a作为返回实例对象b的原型对象,在a中定义属性方法,都能被b实例对象继承下来
            var a = {
                getX:function(){
                    console.log('X');
                }
            };
            var b = Object.create(a);
            b.getX();

2、工厂模式

虽然对象字面量可以用来创建单个对象,但如果要创建多个对象,会产生大量的重复代码

var person1 = {
                name:'小猿',
                age:20,
            }
var person2 = {
                name:'小林',
                age:30,
            }
var person3 = {
                name:'小黎',
                age:22,
            }

为了解决上述问题,人们开始使用工厂模式。该模式抽象了创建具体对象的过程,用函数来封装以特地接口创建对象的细节

            function creatPerson(name,age){
                var o = new Object();
                o.name = name;
                o.age = age;
                o.sayName = function(){
                    console.log(this.name);
                }
                o.sayAge = function(){
                    console.log(this.age);
                }
                return o;

            }

            var p1 = creatPerson('小明',20);
            var p2 = creatPerson('轻轻',30);
            p1.sayName();
            p1.sayAge();
            p2.sayName();
            p2.sayAge();

工厂模式虽然解决了创建多个相似对象的问题,但没有解决对象识别的问题,因为使用该模式并没有给出对象的类型

3、构造函数公式

可以通过创建自定义的构造函数,来定义自定义对象类型的属性和方法。创建自定义的构造函数意味着可以将它的实例标识为一种特定的类型,而这正是构造函数模式胜过工厂模式的地方。该模式没有显式地创建对象,直接将属性和方法赋给了this对象,且没有return语句。

缺点:使用构造函数的主要问题是每个方法都要在每个实例上重新创建一遍,创建多个完成相同任务的方法完全没有必要,浪费内存空间

   function creatPerson(name,age){
            this.name = name;
            this.age = age;
            this.sayName = function(){
                console.log(this.name);
            }
            this.sayAge = function(){
                console.log(this.age);
            }
        }
        var man = new creatPerson('mjj',15);
        var woman = new creatPerson('mm',20);
         //具有相同的sayName()方法在man和woman这两个实例中缺占用了不同的内存空间
        console.log(man,woman);
        console.log(man.sayName === woman.sayName);//false,分别为两块不同的内存地址

(1)构造函数拓展模式

在构造函数模式的基础上,把方法定义转移到构造函数外部,可以解决方法被重复创建的问题

   function creatPerson(name,age){
            this.name = name;
            this.age = age;
            this.sayName = sayName;
            };
//定义多个全局函数,严重污染全局空间
   function sayName(){
              console.log(this.name);
            };
   function sayAge(){
              console.log(this.age);
            }   

        var man = new creatPerson('mjj',15);
        var woman = new creatPerson('mm',20);

        console.log(man,woman);
        console.log(man.sayName === woman.sayName);//true

缺点:定义多个全局函数,严重污染全局空间

(2)寄生构造函数模式

创建一个函数,函数体内部创建一个对象,并且在函数体当中返回,在外部的时候使用new关键字实例化对象。

特点:结合了工厂模式和构造函数模式

问题:1、定义了相同的方法,浪费内存空间。 2、instanceof运算符和prototype属性都没有意义

      //(2)寄生构造函数模式
        function creatPerson(name, age) {
            var o = new Object();
            o.name = name;
            o.age = age;
            o.sayName = function (){
            console.log(this.name);
            }
            o.sayAge = function (){
                console.log(this.age);
            }
            return o;
        }
        var man = new creatPerson('mjj', 15);
        var woman = new creatPerson('mm', 20);

        console.log(man, woman);
        console.log(man.sayName === woman.sayName);//fals
        console.log(man.__proto__ === creatPerson.prototype);//fals
        console.log(man instanceof creatPerson);//fals

(3)稳妥构造函数模式

没有公共属性,并且它的方法也不引用this对象

所谓稳妥对象指没有公共属性,而且方法也不引用this对象。稳妥对象最适合在一些安全环境中(这些环境会禁止使用this和new)或者在防止数据被其他应用程序改动时使用

稳妥构造函数与寄生构造函数模式相似,但有两点不同,一是新创建对象的实例方法不引用this;二是不适用new操作符调用构造函数

function Person(name,age){
    //创建要返回的对象
    var p = new Object();
    //可以在这里定义私有变量和函数
    //添加方法
    //name属于私有属性,结合闭包
    p.sayName = function (){
        console.log(name);
    }
    //返回对象
    return p;
}
//在稳妥模式创建的对象中,除了使用sayName()方法之外,没有其他方法访问name的值,p1对象叫做稳妥模式
var p1 = Person('mjj',28);
p1.sayName();//"mjj"

与寄生构造函数模式相似,使用稳妥构造函数模式创建的对象与构造函数之间也没有什么关系,因此instanceof操作符对这种对象也没有什么意义

4、原型模式

使用原型对象,可以让所有实例共享它的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中

function Person(){
    Person.prototype.name = "mjj";
    Person.prototype.age = 29;
    Person.prototype.sayName = function(){
        console.log(this.name);
    }
}
var p1 = new Person();
p1.sayName();//"mjj"
var p2 = new Person();
p2.sayName();//"mjj"
alert(p1.sayName === p2.sayName);//true
更简单的原型模式

为了减少不必要的输入,也为了从视觉上更好地封装原型的功能,用一个包含所有属性的方法的对象字面量来重写整个原型对象

但是,经过对象字面量的改写后,constructor不再指向Person。因此此方法完全重写了默认的prototype对象,使得Person.prototype的自有属性constructor属性不存在,只有从原型链中找到Object.prototype中的constructor属性

function Person(){};
Person.prototype = {
    name:'mjj',
    age:28,
    sayName:function(){
        console.log(this.name);
    }
}
var p1 = new Person();
p1.sayName();//"mjj"
console.log(p1.constructor === Person);//false
console.log(p1.constructor === Object);//true

可以显示地设置原型对象的constructor属性

     function person() {

        };
        Person.prototype = {
            constructor: Person,
            name: "mjj",
            age: 29,
            sayName: function () {
                console.log(this.name);
            }
        }
        var p1 = new Person();
        p1.sayName();//"mjj"
        var p2 = new Person();
        p2.sayName();//"mjj"
        console.log(p1.sayName === p2.sayName);//true
        console.log(p1.constructor === Person);//true

原型模式问题在于引用类型值属性会被所有的实例对象共享并修改,这也是很少有人单独使用原型模式的原因。

function Person(){};
Person.prototype = {
    constructor:Person,
    name:'mjj',
    age:28,
    friends:['alex','阿黄'],
    sayName:function(){
        console.log(this.name);
    }
}
var p1 = new Person();
var p2 = new Person();
p1.friends.push('阿黑');
alert(p1.friends);//['alex','阿黄','阿黑']
alert(p2.friends);//['alex','阿黄','阿黑']
alert(p1.friends === p2.friends);//true

5、组合模式

组合使用构造函数模式原型模式是创建自定义类型的最常见方式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性,这种组合模式还支持向构造函数传递参数。实例对象都有自己的一份实例属性的副本,同时又共享对方法的引用,最大限度地节省了内存。该模式是目前使用最广泛、认同度最高的一种创建自定义对象的模式

        //5、组合模式
        function person(name,age){
            //定制当前对象自己的属性
            this.name = name;
            this.age = age;
            this.friends = ['mjj','Alxe'];
        }
        //定义各个实例对象共享的属性
        person.prototype = {
            //改变原型对象的同时要改变原型对象 constructor属性,让它指向当前的构造函数person
            constructor : person,
            sayName :function(){
                console.log(this.name);
            }
        }

        var wo = new person('wo',22);
        var you = new person('mjj',28);
        console.log(wo);
        console.log(you);
        wo.friends.push('小米');
        console.log(wo.friends);// ["mjj", "Alxe", "小米"]
        console.log(you.friends);//["mjj", "Alxe"]

6、动态原型模式

动态原型模式将组合模式中分开使用的构造函数和原型对象都封装到构造函数中,然后通过检查方法是否被创建,来决定是否初始化原型对象

使用这种方法将分开的构造函数和原型对象合并到了一起,使得代码更加整齐,也减少了全局控件的污染

注意:如果原型对象中包含多个语句,只需要检查其中一个语句即可

function Person(name,age){
    //属性
    this.name = name;
    this.age = age;
    //方法
    if(typeof this.sayName != "function"){
        Person.prototype.sayName = function(){
            console.log(this.name);
        }
    }
}
var p1 = new Person('mjj',28);
p1.sayName();//"mjj"

总结

1、字面量方式:

问题:创建多个对象会造成代码冗余;

2、工厂模式

解决对象字面量方式创建对象的问题

问题:存在对象识别的问题;

3、构造函数模式

解决了象识别的问题

问题:关于方法的重复创建问题

4、原型模式

解决构造函数模式创建对象的问题

特点:在于方法可以被共享

问题:引用类型值属性会被所有的实例对象共享并修改

5、组合模式(构造函数模式和原型模式)

构造函数模式:定义实例属性

原型模式:用于定义方法和共享的属性,还支持向构造函数中传递参数

此外还有构造函数扩展模式、寄生函数模式、稳妥构造函数模式、动态原型模式

7、基于面向对象的选项卡案例

面向过程选项卡实现

html部分

<!DOCTYPE>
<html>

<head>
    <meta charset="UTF-8">
    <title>选项卡案例</title>
    <link rel="stylesheet" href="css/reset.css">
    <style>
        body {
            background-color: #b39c8a;
        }

        #warp {
            width: 302px;
            height: 342px;
            margin: 100px auto;
        }

        ul {
            width: 300px;
            height: 40px;
            overflow: hidden;
            border: 1px solid #2b75b8;
        }

        #warp ul li {
            width: 100px;
            height: 40px;
            float: left;
            text-align: center;
            line-height: 40px;
        }

        #warp ul .active {
            background-color: #2b75b8;
            font-weight: bold;
        }

        #warp ul li a {
            display: inline-block;
            width: 100px;
            height: 40px;
            /* border: 1px solid #2b75b8; */
            color: #262626;
            font-size: 18px;
        }

        #warp .content {
            width: 300px;
            height: 300px;
            border: 1px solid #2b75b8;
            display: none;
            margin-top: -1px;
        }
    </style>
</head>

<body>
    <div id="warp">
        <ul>
            <li class="active">
                <a href="javascript::void(0);">推荐</a>
            </li>
            <li>
                <a href="javascript::void(0);">小说</a>
            </li>
            <li>
                <a href="javascript::void(0);">导航</a>
            </li>
        </ul>
        <div class="content" style="display:block;"> 推荐</div>
        <div class="content">小说</div>
        <div class="content">导航</div>
    </div>
    </body>

</html>

慢慢改成面向对象的形式

封装:将函数和方法分离

window.onload = function(){    
 // 1.获取需要的标签    
    var tabLis = document.getElementsByTagName('li');    
    var contentDivs = document.getElementsByClassName('content');    
    for(var i = 0; i < tabLis.length; i++){        
        // 保存每个i        
        tabLis[i].index = i;        
        tabLis[i].onclick = clickFun;    
    }    
    function clickFun(){        
        for(var j = 0; j < tabLis.length;j++){            
            tabLis[j].className = '';            
            contentDivs[j].style.display = 'none';        
        }        
        this.className  = 'active';        
        contentDivs[this.index].style.display = 'block';            
    }
}

基于面向对象来实现

思路:1.创建一个TabSwitch的构造函数
	2.给当前对象添加属性(状态:比如绑定的html元素)
	3.给当前对象的原型对象上添加方法(点击方法)
window.onload = function(){    
    // 1.创建构造函数    
    function TabSwitch(obj){        
        console.log(obj);        
     // 2.绑定实例属性        
     this.tabLis = obj.children[0].getElementsByTagName('li');        
        this.contentDivs = obj.getElementsByTagName('div');        
        for(var i = 0; i < this.tabLis.length; i++){            
            // 保存每个i            
            this.tabLis[i].index = i;            
            this.tabLis[i].onclick = this.clickFun;        
        }    
    }    
    TabSwitch.prototype.clickFun = function(){        
        // 去掉所有        
        for(var j = 0; j < this.tabLis.length;j++){            
            this.tabLis[j].className = '';            
            this.contentDivs[j].style.display = 'none';        
        }        
        this.className  = 'active';        
        this.contentDivs[this.index].style.display = 'block';            
    }    
    var wrap = document.getElementById('wrap');    
    var tab = new TabSwitch(wrap);}

当你感觉自己写的非常完美,在网页上一运行,发现报错了

这是因为在clickFun此时是指向了当前点击的li标签,而我们希望此方法中的this指向了tab对象。

clickFun的调用放在一个函数里,这样就不会改变clickFun的所属对象了。同时,还会存在另一个问题,此时的clickFun的this指向了tab对象,但是this.className,this.index,此处的this应该指向的是tab对象,那么但tab对象中没有这两个属性。所以以下改造才正确

// 1.创建TabSwitch构造函数function 
TabSwitch(id){    
    // 保存this    
    var _this = this;    
    var wrap = document.getElementById(id);    
    this.tabLis = wrap.children[0].getElementsByTagName('li');    
    this.contentDivs = wrap.getElementsByTagName('div');    
    for(var i = 0; i< this.tabLis.length; i++){        
        // 设置索引        
        this.tabLis[i].index = i;        
        // 给按钮添加事件       
        this.tabLis[i].onclick = function(){           
            _this.clickFun(this.index);       
        }    
    }
}
// 原型方法
TabSwitch.prototype.clickFun = function(index){    
    // 去掉所有    
    for(var j = 0; j < this.tabLis.length;j++){        
        this.tabLis[j].className = '';        
        console.log(this.contentDivs)        
        this.contentDivs[j].style.display = 'none';    
    }    
    this.tabLis[index].className  = 'active';    
    this.contentDivs[index].style.display = 'block';    
};
new TabSwitch('wrap');

最终版

将代码提取到一个单独的js文件中,在用的时候引入即可

五、实现继承的五种方式

继承是指在原型对象的所有属性和方法,都能被实例对象共享。也就是说,我们只要在原有对象的基础上,略作修改,得到一个新的的对象。

1、原型链继承

JavaScript使用原型链作为实现继承的主要方法,实现的本质是重写原型对象,代之以一个新类型的实例。下面代码中,原来存在于SuperType的实例对象的属性和方法,现在也存在于SubType.prototype中了

        function Animal(){
            this.name = 'alex';
        }

        Animal.prototype.getName = function(){
            return this.name;
        }
        function Dog(){};
        //Dog继承了Animal

        //本质:重写原型对象,将一个父对象的属性和方法作为一个子对象的原型对象的属性和方法
        Dog.prototype = new Animal();
        Dog.prototype.constructor = Dog;
        var d1 = new Dog();
        console.log(d1.name);
        console.log(d1.getName);

以上代码定了两个类型:Animal和 Dog。Dog继承了Animal,而继承是通过创建Animal实例,并将实例赋给Dog.prototype实现的。**实现的本质是重写对象,代之以一个新类型的属性。**换句话说,原来存在于Animal的实例中的所有属性和方法,现在也存在与Dog.prototype中。如图所示。

上图可以看出,我们没有使用Dog默认提供的原型,而是给它换了一个新原型;这个新原型就是Animal的实例。于是,新原型不仅具有作为一个Animal的实例所拥有的属性和方法,而且它还指向了Animal的原型。最终结果就是这样的:

d1=>Dog的原型=>Animal的原型

一个实例属性,而getName()则是一个原型方法。既然Dog.prototype现在是Animal的实例,那么Name位于该实例中。

此外,要注意d1.constructor现在指向的 是 Animal,这是因为原来Dog.prototype 中的 constructor 被重写了的缘故。

原型链最主要的问题私有原型属性会被实例共享,而这也正是为什么要在构造函数中,而不 是原型对象中定义属性的原因。在通过原型来实现继承时,原型实例会变成另一个类型的实例。于是,原先的实例属性也就顺理成章的变成了现在的原型属性了。

原型链的第二个问题,在创建子类型的实例时,不能向父类型的构造函数传递参数。实际上,应该说是没有办法在不影响所有都想实例的情况下,给父类型的构造函数传递参数。再加上包含引用类型值的原型属性会被所有实例共享的问题,在实践中很少会单独使用原型链继承

        function Animal(){
            this.name = 'alex';
            this.colors = ['blue','green','red']
        }

        Animal.prototype.getName = function(){
            return this.name;
        }
        function Dog(){};
        //Dog继承了Animal

        //本质:重写原型对象,将一个父对象的属性和方法作为一个子对象的原型对象的属性和方法
        Dog.prototype = new Animal();
        Dog.prototype.constructor = Dog;
        var d1 = new Dog();
        var d2 = new Dog();
        console.log(d1.name);
        console.log(d1.getName);
        d1.colors.push('black');
        console.log(d1.colors);//["blue", "green", "red", "black"]
        console.log(d2.colors);//["blue", "green", "red", "black"]

      /*  问题:
          1、父类中的实例属性,一旦赋值给子类的原型属性,此时这些属性都属于子类的共享属性
          2、实例化子类型的时候,不能向父类型的构造函数传参

      **/

注意问题

使用原型链继承方法要谨慎地定义方法,子类型有时候需要重写父类的某个方法,或者需要添加父类中不存在的某个方法。但不管怎样,给原型添加方法的代码一定要放在替换原型的语句之后。

function Super() {
    this.colors = ['red', 'green', 'blue'];
}
Super.prototype.getValue = function() {
    return this.colors
}
function Sub() {
    this.colors = ['black'];
};
//Sub继承了Super
Sub.prototype = new Super();
//添加父类已存在的方法,会重写父类的方法
Sub.prototype.getValue = function() {
    return this.colors;
}
//添加父类不存在的方法
Sub.prototype.getSubValue = function(){
    return false;
}
var ins = new Sub();
//重写父类的方法之后得到的结果
console.log(ins.getValue()); //['black']
//在子类中新定义的方法得到的结果
console.log(ins.getSubValue());//false
//父类调用getValue()方法还是原来的值
console.log(new Super().getValue());//['red', 'green', 'blue']

2、借用构造函数继承

只能继承属性,不能继承方法

缺点:父类定义的共享方法不能被子类继承下来

借用构造函数的技术(有时候也叫做伪类继承或经典继承)。这种技术的基本思想相当简单,即在子类构造函数的内部调用父类构造函数。别忘了,函数只不过是在特定环境中执行代码的对象,因此通过使用apply()call()方法也可以在新创建的对象上执行构造函数。

      //2、借用继承模式
      //经典继承:在子类的构造函数内部调用父类的构造函数
        function Animal(){
            this.name = 'alex';
            this.colors = ['blue','green','red']
        }

        Animal.prototype.getName = function(){
            return this.name;
        }
        function Dog(){
            //继承了Animal
            Animal.call(this);
        }
        var d1 = new Dog();
        console.log(d1.name);
 		console.log(d1.getName);//getName is not a function

借用构造模式解决了原型链模式的两个问题

      //解决了原型链继承的第一个问题
        function Animal(){
            this.name = 'alex';
            this.colors = ['blue','green','red']
        }

        Animal.prototype.getName = function(){
            return this.name;
        }
        function Dog(){
            //继承了Animal
            Animal.call(this);
        }
        var d1 = new Dog();
        var d2 = new Dog();
        d1.colors.push('black');
        console.log(d1.colors);// ["blue", "green", "red", "black"]
 		console.log(d2.colors);// ["blue", "green", "red"]

      //解决了原型链继承的第二个问题(可以传递参数)
        function Animal(name){
            this.name = name;
            this.colors = ['blue','green','red']
        }

        Animal.prototype.getName = function(){
            return this.name;
        }
        function Dog(name){
            //继承了Animal
            //当new实例的时候,内部构造函数中的this指向了d1,然后在当前构造函数内部再去通过call再去调用函数,那么父类中的鼓噪函数中的this指向了d1,但是方法不能被继承下来
            Animal.call(this,name);
        }
        var d1 = new Dog('小明');
        var d2 = new Dog('小红');
        d1.colors.push('black');
        console.log(d1.colors);// ["blue", "green", "red", "black"]
        console.log(d2.colors);// ["blue", "green", "red"]
        console.log(d1.name);//小明
        console.log(d2.name);//小红

借用构造函数的问题

如果仅仅是借用构造函数,那么将无法避免构造函数模式存在的问题——方法都在构造函数中定义,因此函数复用就无从谈起。而且,在父类的原型中定义的方法,对子类而言是不可见的。所以这种方式使用较少。

3、组合继承(重要)

将原型链继承和借用构造函数继承组合使用,避免了原型链和借用构造函数的缺陷,融合了他们的优点

        function Animal(name) {
            this.name = name;
            this.colors = ['blue', 'green', 'red'];
        }

        Animal.prototype.getName = function () {
            return this.name;
        }
        function Dog(name) {
            //继承了Animal
            //让父类的实例属性继承下来,实例修改引用类型的值,另一个实例的引用类型值不会发生变化
            Animal.call(this, name);
        }
		//重写原型对象:把父类的共享方法继承下来
        Dog.prototype = new Animal();
        Dog.prototype.constructor = Dog;
        var d1 = new Dog('小明');
        var d2 = new Dog('小红');
        d1.colors.push('black');
        console.log(d1.colors);// ["blue", "green", "red", "black"]
        console.log(d2.colors);// ["blue", "green", "red"]
        console.log(d1.name);//小明
        console.log(d2.name);//小红
        console.log( d1.getName());//小明
        console.log( d2.getName());//小红

组合继承的问题

无论在什么情况下,都会调用两次父类的构造函数:一次是在创建子类原型的时候,另一次是在子类构造函数内部。

4、寄生组合式继承

寄生组合被认为是最完美的继承方案,是开发中应用最多的方法

Object.create()

利用**Dog.prototype = Object.create(Animal.prototype);**来解决组合模式当中,Animal函数被调用两次的缺点

           function Animal(name) {
            this.name = name;
            this.colors = ['blue', 'green', 'red'];
        }

        Animal.prototype.getName = function () {
            return this.name;
        }
        function Dog(name) {
            //继承了Animal
            Animal.call(this, name);
        }
        Dog.prototype = Object.create(Animal.prototype);
        Dog.prototype.constructor = Dog;
        var d1 = new Dog('小明');
        var d2 = new Dog('小红');
        d1.colors.push('black');
        console.log(d1.colors);// ["blue", "green", "red", "black"]
        console.log(d2.colors);// ["blue", "green", "red"]
        console.log(d1.name);//小明
        console.log(d2.name);//小红
        console.log( d1.getName());//小明
        console.log( d2.getName());//小红

继承总结

1、原型链继承

特点:重写子类的原型对象,父类原型对象上的属性和方法都会被子类继承

问题:1、在父类中定义的实例引用类型的属性,一旦被修改,其他的实例也会被修改

2、当实例化子类的时候,不能传递参数到父类

2、借用构造函数继承

特点:在子类函数内部,间接调用(call()、apply()、bind())父类的构造函数

原理:改变父类中的this指向

优点:仅仅的是把父类中的实例属性当做子类的实例属性,并且还能传参

问题:父类中共有的方法不能被继承下来

3、组合继承

特点:结合了原型链继承和借用构造函数继承的优点,

原型链继承:公有的方法能被继承下来

借用构造函数继承:实例属性能被子类继承下来

问题:调用了两次父类的构造函数

1、第一次是初始化子类原型的时候时候

2、子类的构造函数内部(好)

4、寄生组合模式

var b = Object.createa);

将a对象作为b实例的原型对象

把子类的原型对象指向了 父类的原型对象

Dog.prototype = Object.create(Animal.prototype)

寄生组合:是开发过程中使用最多最广泛的继承模式。

5、多重继承

JavaScript中不存在多重继承,那也就意味着一个对象不能同时继承多个对象,但是我们可以通过变通方法来实现。

Object.assign(targetObj,copyObj) 前面一个值为复制到某一个地方的值,第二个值为被复制的对象

       //5、多重继承
        // 多重继承:一个对象同时继承多个对象
        // Person  Parent  Me
        //定制person
        function Person(){
            this.name = 'Person';
        }
        Person.prototype.sayName = function(){
            return this.name
        }

        //定制Parent
        function Parent(){
            this.age = 30;
        }
        Parent.prototype.sayAge = function(){
            return this.age
        }

        function Me(){
            // 继承Person的属性
            Person.call(this);
            // 继承Parent的属性
            Parent.call(this);
        }
        // 继承Person的方法
        Me.prototype = Object.create(Person.prototype);
        // 不能重写原型对象来实现 另一个对象的继承
        // Me.prototype = Object.create(Parent.prototype);
        // Object.assign(targetObj,copyObj)
        Object.assign(Me.prototype,Parent.prototype);
        // 指定构造函数
        Me.prototype.constructor = Me;
        var me = new Me;
        console.log(me.name);//Person
        console.log(me.sayName());//Person
        console.log(me.age);//30
        console.log(me.sayAge());//30