JavaScript的封装,继承 | 原型链

Javascript面向对象编程 之 (封装)

 

Javascript是一种基于对象(object-based)的语言,你遇到的所有东西几乎都是对象。但是,它又不是一种真正的面向对象编程(OOP)语言,因为它的语法中没有class(类)。

那么,如果我们要把"属性"(property)和"方法"(method),封装成一个对象,甚至要从原型对象生成一个实例对象,我们应该怎么做呢?

一、 生成实例对象的原始模式

 

<script>
    //假定我们把猫看成一个对象,它有"名字"和"颜色"两个属性。
    var cat = {
        name: '',
        color:''
    }
</script>

现在,我们需要根据这个原型对象的规格(schema),生成两个实例对象。

<script>
    var cat1 = {}; // 创建一个空对象,var cat1={}是一种简写,它是一种语法糖,它实际上等于 var cat1=new Object()
    cat1.name = "旺财"; // 按照原型对象的属性赋值
    cat1.color = "黄色";

    var cat2 = {};
    cat2.name = "来福";
    cat2.color = "黑色";
</script>

好了,这就是最简单的封装了,把两个属性封装在一个对象里面。但是,这样的写法有两个缺点,一是如果多生成几个实例,写起来就非常麻烦;二是实例与原型之间,没有任何办法,可以看出有什么联系。

 

 

二、 原始模式的改进

<script>
    function Cat(name, color) {
        return{
            Name : name,
            Color : color}
    }
    //现在我们要生成两个猫的实例对象
    var cat1 = new Cat("旺财", '黄色');

    var cat2 = new Cat("来福", '黑色');

    alert(cat1.Name);//打印出:旺财
    alert(cat2.Name);//打印出:来福

</script>

这种方法的问题依然是,cat1和cat2之间没有内在的联系,不能反映出它们是同一个原型对象的实例

三、 构造函数模式

为了解决从原型对象生成实例的问题,Javascript提供了一个构造函数(Constructor)模式。
所谓"构造函数",其实就是一个普通函数,但是内部使用了this变量。对构造函数使用new运算符,就能生成实例,并且this变量会绑定在实例对象上。
比如,猫的原型对象现在可以这样写,

<script>
    function Cat(name, color) {
        this.Name = name;
        this.Color = color;
    }
    //现在我们要生成两个猫的实例对象
    var cat1 = new Cat("旺财", '黄色');

    var cat2 = new Cat("来福", '黑色');

    alert(cat1.Name);//打印出:旺财
    alert(cat2.Name);//打印出:来福

    //这时cat1和cat2会自动含有一个constructor属性,指向它们的构造函数。(下面就来验证一下)
    alert(cat1.constructor === Cat) //打印出:true
    alert(cat2.constructor === Cat) //打印出:true

    //Javascript还提供了一个instanceof运算符,验证原型对象与实例对象之间的关系。
    alert(cat1 instanceof Cat); //true
    alert(cat2 instanceof Cat); //true
</script>

 

四、构造函数模式的问题


构造函数方法很好用,但是存在一个浪费内存的问题。
请看,我们现在为Cat对象添加一个不变的属性type(种类),再添加一个方法eat(吃)。那么,原型对象Cat就变成了下面这样:

<script>
    function Cat(name, color) {
        this.Name = name; //不同的猫有不同的名字,   那么其中的this就代表它即将new出来的对象
        this.Color = color; //不同的猫有不同的颜色

        this.Type = '猫科动物'; //所有猫都有一致的类型属性
        this.Eat = '吃老鼠';    //所有猫都有一致的行为属性
    }
    //现在我们要生成两个猫的实例对象
    var cat1 = new Cat("旺财", '黄色');

    var cat2 = new Cat("来福", '黑色');

    alert(cat1.Eat); //打印出:吃老鼠
    alert(cat2.Eat); //打印出:吃老鼠

    alert(cat1.Eat == cat2.Eat); //打印出:false  那是因为cat1和cat2是不同的对象
</script>

表面上好像没什么问题,但是实际上这样做,有一个很大的弊端。那就是对于每一个实例对象,type属性和eat()方法都是一模一样的内容,每一次生成一个实例,都必须为重复的内容,多占用一些内存。这样既不环保,也缺乏效率。
能不能让type属性和eat()方法在内存中只生成一次,然后所有实例都指向那个内存地址呢?回答是可以的。

 

 

五、 Prototype模式

 

<script>
    function Cat(name, color) {
        this.Name = name; //不同的猫有不同的名字
        this.Color = color; //不同的猫有不同的颜色      
    }

    Cat.prototype.Type = '猫科动物'; //所有猫都有一致的类型属性
    Cat.prototype.Eat = '吃老鼠';    //所有猫都有一致的行为属性

    //现在我们要生成两个猫的实例对象
    var cat1 = new Cat("旺财", '黄色');

    var cat2 = new Cat("来福", '黑色');

    alert(cat1.Eat); //打印出:吃老鼠
    alert(cat2.Eat); //打印出:吃老鼠

    alert(cat1.Eat == cat2.Eat); //打印出:true  这时所有实例的type属性和eat()方法,其实都是同一个内存地址,指向prototype对象
</script>

这时所有实例的type属性和eat()方法,其实都是同一个内存地址,指向prototype对象,因此就提高了运行效率。

 

 

JS核心系列:浅谈 原型对象和原型链

 

 

一、函数的定义

Javascript中,万物皆对象,但对象也有区别,大致可以分为两类,即:普通对象(Object)和函数对象(Function)

一般而言,通过new Function产生的对象是函数对象,其他对象都是普通对象【注:对象是通过函数来创建的,Object,Array都是函数】

 

<script>
    //Function是JS自带的对象,f1,f2在创建的时候,JS会自动通过new Function()的方式来构建这些对象,
    //因此,这f1,f2,f3这三个对象都是通过new Function()创建的。

    function f1() { }; //最常见的函数定义方式

    var f2 = function () { }; //定义了一个匿名函数

    var f3 = new Function('x', 'y', 'return x+y'); //不常见,但也是一种函数的创建方式,它相当于var f3=function(x,y){ return x+y};



    var a1 = {}; //创建一个空对象,var cat1={}是一种简写,它是一种语法糖,它实际上等于 var cat1=new Object()
    var a4 = { name: '旺财', age: 2 };//它实际等于 var a4=new Object(); a4.name='旺财';a4.age=2;

    var a2 = new Object();
    var a3 = new f1();

    alert(typeof (f1)); //打印出:function  函数对象
    alert(typeof (f2)); //打印出:function  函数对象
    alert(typeof (f3)); //打印出:function  函数对象

    alert(typeof (a1));//打印出:object  普通对象
    alert(typeof (a2));//打印出:object  普通对象
    alert(typeof (a3));//打印出:object  普通对象

</script>

二、函数与对象的关系

 

function Dog(name,age) {
    this.Name = name;
    this.Age = age;
}

//创建自定义对象
var dog = new Dog("旺财", 2);

//当代码 var dog = new Dog("旺财", 2)执行的时候,其实内部做了如下几件事情:
//1>创建了一个空白对象 new Object()
//2>拷贝Dog.prototype中的属性(键值对)到这个空对象中(我们前面提到,内部实现时不是拷贝而是一个隐藏的链接)[这就是我们说的继承]
//3>将这个对象通过this关键字传递到构造函数中并执行构造函数。
//4>将这个对象赋值给变量dog

通过以上注释可以得出:当执行var dog=new Dog('旺财',2)的时候,其实我们是创建了一个空对象,然后这个空对象继承了Dog函数对象。所以我们可以通过dog.Name得到Dog对象中的属性

 

 

三、Javascript中的原型和原型链

通过以上的之后,我们再来了解一下Javascript中的原型和原型链:

 

JS中,每声明一个函数的时候就会默认内置了一个prototype属性,这个prototype的属性值是一个对象(属性的集合,再次强调!),默认的只有一个叫做constructor的属性,指向这个函数本身。一般construtor就是我们平时对函数设置的实例化对象(我理解为:这个prototype属性指向了函数本身,它就是这个函数的对象)

那么我们在创建这个函数对象的时候(即:new 一个函数对象)它会自动在对象里内置一个__proto__的属性,这个属性是隐藏的,这个属性又指向了该函数的原型prototype

 

function f1() { }//通过function定义的对象是有prototype属性的,我们可以通过 f1.prototype 来点出来

var v = new f1(); //通过使用new操作符生成的对象是没有prototype属性的,原因是他已经实例化,但是它有一个隐藏的__proto__属性,可以硬性的调出来

alert(f1.prototype === v.__proto__)//打印出:true 表示v的__proto__属性的确指向了f1函数的原型 即:f1.prototye

看下面的例子

 

 

function Dog() { 
    this.Name= '旺财';//创建一个函数,并给这个函数添加了一个属性Name
}

Dog.prototype.Age = '2' //创建这个Dog函数的时候,函数内部就自动添加了一个prototype属性,这个属性也是一个对象,它指向了函数本身,所以我们可以通过.的形式给函数添加属性,或者方法

Dog.prototype.Eat = function () { //给Dog函数添加一个Eat属性,这个属性的值是一个匿名函数,即:Eat指向了一个函数
    alert("吃骨头");
}

var dog = new Dog();//当我们创建这个Dog对象的时候,JS会自动给内置一个__proto__属性,这个属性是隐藏的,这个属性就指向了Dog的原型,即:指向了Dog.prototype

alert(dog.Age) //当我们通过dog.Age去Dog对象中找这个Age属性的时候,发现找不到,于是就去隐藏的__proto__属性(对象)中找,于是就找到了这个Age 而__proto__又有自己的__proto__,假如没有找到,就会去__proto__的__proto__中找,一直于是就这样 一直找下去,也就是我们平时所说的原型链的概念。【其实prototype只是一个假象,他在实现原型链中只是起到了一个辅助作用,换句话说,他只是在new的时候有着一定的价值,而原型链的本质,其实在于__proto__】

alert(Dog.prototype === dog.__proto__)//打印出:true 表示dog的__proto__属性的确指向了Dog函数的原型 即:Dog.prototye
console.log(dog)

 

 

 

 

看一个例子:

 

 

 

 

function C1(name) {
    if (name) this.name = name;
}
function C2(name) {
    this.name = name;
}
function C3(name) {
    this.name = name || 'john';
}
//
C1.prototype.name = "Tom";
C2.prototype.name = "Tom";
C3.prototype.name = "Tom";
alert((new C1().name) + (new C2().name) + (new C3().name)); //这里打印出Tom undefined john

//想想为什么? 
//1> newC1().name 这个括号中没有带值,所以根据if语句的条件,C1中没有声明name属性,而C1的原型中又声明了name属性,所以C1函数的实例对象在读取name属性的时候会从实例对象本身开始找这个name,结果没有找到,接着又去C1的prototype原型中找,找到了这个name,所有这个name是Tom

//2> new C2().name 这个括号里没有带值,而C2声明了一个name属性,根据赋值得到 name的值为undefined,所以C1函数的实例对象在读取name属性的时候会从对象实例本身开始找这个name 结果找到了这个name,既然找到了就不会去原型中找了,所有这个name 是undefined

//3>同上,name的值为john

 

Javascript实现继承的三种方式

 

第一种:原型链继承

 

<script>
    //原型链继承
    function Animal() {
        this.species = "动物";
    }

    function Cat(name, color) {
        this.name = name;
        this.color = color;
    }

    Cat.prototype = new Animal(); //如果"猫"的prototype对象指向一个Animal的实例,那么所有"猫"的实例,就能继承Animal了。
    Cat.prototype.constructor = Cat;

    var cat1 = new Cat('小黄', '黄色');
    alert(cat1.species)//打印出:动物
    /*
     注解:Cat.prototype = new Animal(); 这个new Animal()做了什么事情?
     第一:创建了一个空对象,即:new Object()
     第二:将Animal.prototype中的属性(键值对)拷贝到这个空对象中(我们前面提到,内部实现时不是拷贝而是一个隐藏的链接)
     第三:将这个对象通过this关键字传递到构造函数中并执行构造函数
     第四:将这个对象赋值给Cat.prototype
     第五:通过以上步骤,这个Cat对象中就有了Animal的所有属性和方法了。

     注解:Cat.prototype.constructor = Cat; 为什么要将Cat原型的构造器设置为Cat呢?
     第一:原来,任何一个prototype对象都有一个constructor属性,指向它的构造函数。如果没有"Cat.prototype = new Animal();"这一行,Cat.prototype.constructor是原本是指向Cat的;但是加了这一行以后,Cat.prototype.constructor指向Animal。
     可以通过 alert(Cat.prototype.constructor == Animal); //true 来验证。
     第二:更重要的是,每一个实例也有一个constructor属性,默认调用prototype对象的constructor属性。
     可以通过: alert(cat1.constructor == Cat.prototype.constructor); // true 来验证
     因此,在运行"Cat.prototype = new Animal();"这一行之后,cat1.constructor也指向Animal!

     这显然会导致继承链的紊乱(cat1明明是用构造函数Cat生成的),因此我们必须手动纠正,将Cat.prototype对象的constructor值改为Cat。这就是Cat.prototype.constructor = Cat 的意思。
     */
</script>

特点:
1.非常纯粹的继承关系,实例是子类的实例,也是父类的实例
2.父类新增原型方法,原型属性,子类都能访问到
3.简单,易于实现
缺点:
1.要想为子类新增属性和方法,必须要在new Animal()这样的语句之后执行,不能放到构造器中
2.无法实现多继承
3.来自原型对象的引用属性是所有实例共享的
4.创建子类实例时,无法向父类构造函数传参
 

 

 

第二种:构造继承

首先我们让来了解下call方法与apply方法的作用:

 

//call方法与apply方法都的作用都是:调用一个对象的一个方法,以另一个对象替换当前对象
//call(需要替换的函数名称,参数一,参数二,参数N)
//apply(需要替换的函数名字,[参数数组]) 
//call 和 apply二者的作用完全一样,只是接受参数的方式不太一样。
function add(a, b) {
    return a + b
}

function sub(a, b) {
    return a - b;
}
alert(add.call(sub, 3, 1)); //打印出:4
alert(add.apply(sub, [3, 1]));//打印出:4

 

实例:

 

//构造继承
function Animal() {
    this.species = "动物";
}

function Cat(name, color) {
    Animal.apply(this,arguments);//Animal在 Cat中执行,实现了Cat继承Animal得功能 
    this.name = name;
    this.color = color;
}
var cat1 = new Cat('小黄', '黄色');
alert(cat1.species)//打印出:动物

特点:
1.解决了原型链继承中,子类实例共享父类引用属性的问题
2.创建子类实例时,可以向父类传递参数
3.可以实现多继承(call多个父类对象)
缺点:
1.实例并不是父类的实例,只是子类的实例
2.只能继承父类的实例属性和方法,不能继承原型属性/方法
3.无法实现函数复用,每个子类都有父类实例函数的副本,影响性能


第三种:组合继承(推荐)

核心:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用

 

//组合继承
function Animal() {
    this.species = "动物";
}

function Cat(name, color) {
    Animal.apply(this);//Animal在 Cat中执行,实现了Cat继承Animal得功能 (这里调用了一次Animal的构造函数)
    this.name = name;
    this.color = color;
}

Cat.prototype = new Animal(); //将父类实例作为子类原型,实现函数复用(这里又调用了一次Animal的构造函数)
var cat1 = new Cat('小黄', '黄色');
alert(cat1.name)//打印出:动物

特点:
1.弥补了方式构造继承的缺陷,可以继承实例属性/方法,也可以继承原型属性/方法
2.既是子类的实例,也是父类的实例
3.不存在引用属性共享问题
4.可传参
5.函数可复用
6.仅仅多消耗了一点内存,性能好
缺点:
调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)

 

继承方式:

  1、拷贝继承:通用型  有new无new都可以用

  2、类式继承:new构造函数---利用构造函数(类)继承的方式

  3、原型继承:无new的对象---借助原型来实现对象继承对象

  属性继承:调用父类的构造函数call

  方法继承:用for in的形式 拷贝继承(jq也用拷贝继承)

 

 

 

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值