我们创建的每个函数都有prototype(原型)属性,这个属性是一个对象,这个对象包含了特定类型的所有实例共享的属性和方法。

可以用prototype创建构造函数对象的原型对象,好处是让对象实例化共享它所包含的属性和方法

意思就是可以不用在构造函数中定义对象,直接把信息添加在原型对象中即可

如果是直接比较构造函数的地址返回的是FALSE,上章已说到,如果使用原型对象的方法创建地址是否一致?

 <script>
        function Fn1(){};
        Fn1.prototype.name = 'Nanchen';
        Fn1.prototype.age = 18;
        Fn1.prototype.sex = '男';
        Fn1.prototype.fn2 = function(){
            return this.name+this.age + this.sex;
        }
        var box1 = new Fn1();
        var box2 = new Fn1();
        console.log(box1.fn2  == box2.fn2);        //TRUE
    </script>

我们在一个已经存在构造器的对象中是不能添加新的属性的,要添加一个新属性需要在构造函数中添加。

prototype继承

所有odejs对象都会从这个原型对象中继承属性和方法:

Date.prototype:Date对象的继承

Array.prototype:Array对象的继承

Person.prototype:Person对象的继承

所有js中的的对象都是位于Object的实例

可以用prototype添加属性和方法

 <script>
        function Fn1(){};
        Fn1.prototype.name = 'Nanchen';
        Fn1.prototype.age = 18;
        Fn1.prototype.sex = '男';
        Fn1.prototype.fn2 = function(){
            return this.name+this.age + this.sex;
        }
        Fn1.prototype.nnationality = 'English';   //给对象的构造函数添加新的属性
    </script>

也可以给对象的构造函数添加新的方法:

 <script>
        function Fn1(){};
        Fn1.prototype.name = 'Nanchen';
        Fn1.prototype.age = 18;
        Fn1.prototype.sex = '男';
        Fn1.prototype.fn2 = function(){
            return this.name+this.age + this.sex;
        }
        Fn1.prototype.name = function() {            //添加新的方法
          return this.name+ " " + this.age;
};
    </script>

下面这种方法是通过prototype继承后实现的 

  <script>
        function Fn1(name,age,sex){
            this.name = name;
            this.age = age;
            this.sex = sex;
            this.fn2 = function(){
                console.log('姓名:'+this.name+ '年龄:'+this.age + '性别' + this.sex);
            }
        }
        // 新增一个新的方法
        Fn1.prototype.fn3 = function(){
            console.log('此方法使用了继承');
        }
        var num = new Fn1('NanChen',13,'男');
        num.fn2();
        num.fn3();
    </script>

效果如下:

 注意:通常用函数体定义属性,就使用prototype定义方法。

建议:那么函数体中可以只包含属性的定义,而方法可以写在不同的代码块中,时代吗更加具有可读性

例如:

function Fn1(a,b){
    this.a = a;
    this.b = b;
}
Fn1.protopyte.c  = function(){
    return this.a + this.b;
}

不要用字面量的方式来定义属性和方法,否则原有属性和方法会被更新

字面量方式参考实用字面量创建对象

__proto__

其次原型对象中有一个默认属性__proto__

1、该属性指向Object构造函数的prototype属性(即Object原型对象)。

Person.prototype.__proto__ == Object.prototype; //true

2、Object原型对象有一个默认属性__proto__,该属性指向null。  

console.log(Object.prototype.__proto__ == null); //true

3、所有函数都是Function构造函数创建出来的对象 。

结论:Object构造函数 的__proto__属性指向 Function构造函数的prototype属性(也就是Function的原型对象)

Person构造函数 的__proto__属性指向 Function构造函数的prototype属性(也是Function的原型对象)

原型链 

当想要得到一个对象属性时候,发现这个对象本身不存在这个属性 ,那么回去该对象的(prototype)属性中去寻找。

看个例子就明白了:

        function Fn1(){};
        Fn1.prototype.name = '我在这里';
        function Fn2(){};
        Fn2.prototype = new Fn1();
        var person = new Fn2();
        console.log(person.name);

当person实例对象中去查找Fn2方法,发现没有值,那么就会在从Fn2中继续查找Fn1的方法

直到找到Fn1中的原型对象输出Fn1里的属性,此代码就是原型链的机制

比如我在家里找口罩,然而家里并没有口罩,于是就拿起手机(proto属性)搜索附近有没有小店(shop.prototype)之类的,走到小店老板说已经卖完了,老板提议我们去超市看看,然后又拿起手机寻找超市,超市(supermarket.prototype),最终找到口罩。
当我们调用person.__proto__的时候,实例会查看自己有没有这个属性,没有的话就会通过__proto__属性找原型对象有没有属性
也就是所有的构造函数的原型链最后都会引用Object构造函数的原型,即可以理解Object构造函数的原型是所有原型链的最底层,即Object.prototype.__proto===null

原型分析:

1、isPrototypeOf()方法:检测一个对象是否存在于另一个对象的原型链中

例:

<script>
        // 声明一个Box构造函数
        function Box(){
            // Box构造函数的原型对象
            Box.prototype.name = 'NanChen';     //添加属性
            Box.prototype.age = 113;
            Box.prototype.fn1 = function(){ //添加方法
                return this.name+this.age;
            }
        }
        //创建实例对象
        var box1 = new Box();
        console.log(box1.name);
		console.log(Box.prototype.isPrototypeOf(box1)); 
		// 返回值为逻辑表达式
        var box2 = new Box();
        // 如果想判断一个对象是否指向该构造函数的原型对象的话,可以使用isPrototypeOf()方法来测试
    </script>

效果如下: 

 只要是实例化对象,就都会指向。虽然可以用对象实例,访问保存在原型中的值,但却不能通过对象实例直接修改原型中的值。那么该怎样修改原型中的值呢

2、如果想要修改原型中的值,可以通过以下两种方法:

方式1:box1.__proto__.name = 'Jack';
方式2:Box.prototype.name = 'Jack';

3、 如果想要box1访问上一级的原型里的值,那么可以使用删除属性delete

delete:删除上个元素

// 声明一个Box构造函数
        function Box(){
            // Box构造函数的原型对象
            Box.prototype.name = 'NanChen';     //添加属性
            Box.prototype.age = 113;
            Box.prototype.fn1 = function(){ //添加方法
                return this.name+this.age;
            }
        }
        //创建实例对象
        var box1 = new Box();
		box1.name = 'Lee'    //替换了box1.name为‘Lee’
        console.log(box1.name);    //Lee
		delete box1.name;        //这里删除了上一级的box1的name也就是Lee
		console.log(box1.name);    //NanChen

 4、hasOwnProperty()

判断构造函数的实例里是否包含给定属性,包含返回true,否则返回false。说白了其实就是判断自身属性是否存在。 

我们在这里可以判断box1中的name属性是否存在;

var box1 = new Box();
box1.name = 'Lee';
console.log(box1.hasOwnProperty('name'));//true

var box2 = new Box();
console.log(box2.hasOwnProperty('name'));//false

 

 5、使用in操作符判断对象是否包含给定属性,包含返回true,否则返回false。但无法确定该属性,是存在于构造函数的实例中还是原型中。

in:

1、单独使用时,用来判断对象属性是否存在,无论是存在实例中还是原型中,返回true或者false。
2、在for-in循环中,获取对象的所有可访问的、可枚举的属性。

例如:判断box1是否存在属性name

console.log('name' in box1);//true

6、结合hasOwnProperty()和in方法,可以判断原型中是否存在给定属性

function isProperty(object, property) { //判断原型中是否存在属性
    return !object.hasOwnProperty(property) && (property in object);
}
console.log(isProperty(box1, 'name')) //true

7、为了让属性和方法更好的体现封装的效果,并且减少不必要的输入,可以使用字面量方式重写原型

function Box() {};
Box.prototype = { //使用字面量的方式重写原型
    name: 'Lee',
    age: 100,
    run: function() {
        return this.name + this.age + '运行中...';
    }
};

8、使用构造函数方式重写原型对象和使用字面量方式重写原型对象,在使用上基本相同,但还是有一些区别。使用构造函数方式重写的原型对象的constructor属性,仍然指向构造函数实例(对象);使用字面量方式重写的原型对象的constructor属性,指向Object

不加constructor: Box时:

Box.prototype = { //使用字面量的方式重写原型
            // constructor: Box,
            name: 'ni',
            age: 100,
            run: function () {
                return this.name + this.age + '运行中...';
            }
        };
        var box3 = new Box();
        console.log(box3.__proto__.constructor == Box); //false
        console.log(box3.__proto__.constructor == Object);//true

9如果想让使用字面量方式重写的原型对象的constructor属性,指向构造函数实例对象 ,那就加上constructor: Box

加constructor: Box时:

Box.prototype = { //使用字面量的方式重写原型
            constructor: Box,    //直接强制指向即可
            name: 'ni',
            age: 100,
            run: function () {
                return this.name + this.age + '运行中...';
            }
        };
        var box3 = new Box();
        console.log(box3.__proto__.constructor == Box); //true
        console.log(box3.__proto__.constructor == Object);//false

为什么使用字面量重写的原型对象,他的constructor属性会指向Object?

每创建一个函数,他的prototype也会被创建,那么原型对象也会自动的获取这个(constructor)属性。所以写出function Box() {};之后,原型对象的constructor属性,是指向构造函数实例(对象)的。而Box.prototype={};这种写法其实就是创建了一个新的原型对象,这个新的原型对象覆盖了Box原来的原型对象,又因为这个新的原型对象没有指定构造函数(即没有设置constructor属性),那么就默认为Object(即constructor属性值为Object)。

10、原型可以被多次重写,后面重写的原型会覆盖之前的。

function Box() { };
        Box.prototype = { //使用字面量的方式重写原型
            name: 'Lee',
            age: 100,
            run: function () {
                return this.name + this.age + '运行中...';
            }
        };
        Box.prototype = { //使用字面量的方式重写原型
            name: 'ni',
            age: 30,
            run: function () {
                return this.name + this.age + '运行中...';
            }
        };
        Box.prototype = { //使用字面量的方式重写原型
            name: 'nihaoma',
            age: 28,
            run: function () {
                return this.name + this.age + '运行中...';
            }
        };
        console.log(Box.prototype.name);

结果:

11、原型中所有属性是被所有实例共享的,这种方法有利有弊。共享对于函数而言非常合适,如果属性包含引用类型,就会存在以下问题:

例如这里添加一个family的数组:

function Box() { };
        Box.prototype = {
            constructor: Box,
            name: 'Lee',
            age: 100,
            family: ['父亲', '母亲', '妹妹'], //添加了一个数组属性
            run: function () {
                return this.name + this.age + this.family;
            }
        };
        var box1 = new Box();
        var box2 = new Box();
        box1.family.push("哥哥");//这里只给box1添加一个新元素,但是box2的返回值和box返回值一致
        console.log(box1.family);    //["父亲", "母亲", "妹妹", "哥哥"]
        console.log(box2.family);    //["父亲", "母亲", "妹妹", "哥哥"]

 box1.family.push("哥哥");  //修改的是原型里面的属性

console.log(box2.family); //共享带来的麻烦,也会给box2进行共享 

解决办法:可以使用“构造函数+原型”的模式创建对象 

<script>
        function Box(name, age) {
            this.name = name;
            this.age = age;
            this.family = ['父亲', '母亲', '妹妹'];
        };
        Box.prototype = {
            run: function () {
                return this.name + this.age + this.family;
            }
        }
        var box1 = new Box('Lee', 100);
        box1.family.push("哥哥");
        var box2 = new Box('Jack', 200);
        console.log(box1.family);
        console.log(box2.family);
    </script>

效果:

 这样就不会影响到box2了。

12、上面说了“原型”模式或者“构造函数+原型”模式创建对象,这两中方式的共同缺点,就是不管你是不是调用了原型的共享方法。在创建对象的同时就会初始化原型中的方法。

解决方案::使用动态原型模式创建对象:把构造函数和原型封装到一起。

<script>
        function Box(name, age) {
            this.name = name;
            this.age = age;
            // this.fn1 = function(){
            //     return this.name +this.age;
            // }
            if (typeof this.run != 'function') { //仅在第一次调用的时候初始化
                console.log('第一次初始化');       //测试用
                Box.prototype.height = 175;      //测试用,为了便于查看原型信息。
                Box.prototype.run = function () {
                    return this.name + this.age + '岁了';
                };
            };
        }
        var box1 = new Box('NanChen', 100);
        console.log(box1.height);
        console.log(box1.run());
        console.log(box1.run());
        var box2 = new Box('Jack', 200); //第二次创建对象,即第二次调用构造函数。
        console.log(box2.run());
        console.log(box2.run());
    </script>

当创建box1对象时(第一次调用),发现fn1属性不存在,然后初始化原型 

当创建box2对象时,发现fn1的方法存在,就不会再初始化原型了、

好处:这样可以得到封装和共享,并且每个属性都是独立的。

注意: 如果此处使用字面量方式重写,,将会不起作用。原因(会切断构造函数实例和新原型之间联系)

以上介绍了使用原型模式“构造函数+原型”模式动态原型模式三种创建对象的方式 。


本文转载:CSDN博客