我们创建的每个函数都有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的方法存在,就不会再初始化原型了、
好处:这样可以得到封装和共享,并且每个属性都是独立的。
注意: 如果此处使用字面量方式重写,,将会不起作用。原因(会切断构造函数实例和新原型之间联系)
以上介绍了使用原型模式、“构造函数+原型”模式、动态原型模式三种创建对象的方式 。