JS 构造函数,原型对象和实例对象
直接看一幅图:
看来这张图和图片上的代码,对这些概念有了一个基本的了解,在看看下面的一段代码:
function Person (name) {
this.name = name;
}
Person.prototype.sex = 'boy';
Person.prototype.sayWords = function(des) {
console.log(des+' say words:', this.name,this.sex);
}
let p1 = new Person('p1');
// code 1
p1.sayWords('p1-des1');
console.log("Person实例对象:p1(改变前):",p1);
console.log("Person原型对象:p1.__proto__:",Object.getPrototypeOf(p1));
// code 2
console.log("----------------------")
p1.name = 'p1-1001';
p1.sex = 'girl';
p1.sayWords('p1-des2');
console.log("Person实例对象:p1(改变后):",p1);
console.log("Person原型对象:p1.__proto__:",Object.getPrototypeOf(p1));
结果如下图:
加了code 2部分代码后:
从结果从可以看出,下面两句代码:
p1.name = 'p1-1001';
p1.sex = 'girl';
并没有修改prototype原型对象里面的属性sex的值,原型对象里面属性sex依然是sex:"boy";这两句代码只是对实例对象本身做了修改,把实例对象的name值改为“girl",并且添加了一个sex属性。
从这里就看出来了,写在Person()构造函数内部的那些属性或方法在new操作的过程中赋值给了实例对象p1的,所以构造函数内部的属性方法都是当前实例对象所私有的。
new操作的时候,除了会指向构造函数里面的代码并返回一个实例对象(就是构造函数里面的哪个this)外,还会把构造函数的作用域赋值给创建的实例对象,所以构造函数里面的this.xx = ‘xxx' 都是给实例对象添加属性并赋值,好比这样:
function PersonFn () {
let o = new Object();
o.a = 'aaa';
o.b = 'bbb';
return o;
}
每次执行PersonFn 都会生成一个全新的对象o。
那么code 1那段代码,p1实例对象是怎么拿到原型对象的sex属性的呢?或者说实例对象是怎么访问到原型对象的属性和方法的呢?
答案就是原型链。
当我们new一个函数的时候,这个函数就从一个普通函数变成了构造函数,并且会返回一个实例对象,函数中的this自动指向了这个实例对象,所以构造函数中的方法和属性,用面向对象的语言来说,构造函数里面的属性方法都是实例对象自己的私有成员,上面也说过了;同时实例对象还会有一个隐藏属性叫做[[prototype]]或proto,这个属性指向的就是原型对象,这个隐藏属性就是实现原型链的关键。
还以code 1代码段为例子,p1对象{”name":"p1",proto:原型对象地址},里面更本没有sex这个属性,也没有sayWords这个方法,如果是一般的对象比如:
var px = {};
px.a;
px.fn();
这样的一段代码肯定会报错的,px.a是undefined,px.fn()没这个方法,直接报错。
但是实例对象不会,当代码执行到p1.sayWords()的时候,如果实例对象自己没有这个方法,代码就会通过proto属性,像攀爬锁链一样,继续往上走,走到Person原型对象,原型对象这里是有这个方法的,于是就调用原型对象的这个方法。
再想想,如果Person原型对象也没找到想要的属性方法呢?仔细看看Person原型对象,它也有一个proto属性,这个属性指向的是Object函数的原型对象,同理,代码依然会顺着Person原型对象的proto属性找到Object原型对象,如果Object原型对象也没找到,那么就会报错,因为Object是所有函数的父类,也锁链的最顶层,找不到只能报错了。
说起原型链,又想到作用域链。
作用域链也是类似的,比如:
<script>
var x = 0;
function a (){
x++;
}
a();
</script>
这是一段很简单的代码,如果a函数的作用域中没有变量x,代码就会继续往上层作用域查找,直到找到变量x或者一直找到顶层作用域,即window对象,如果window对象中也没有要查找的变量,就会报错。
再说个方法:某个对象.hasOwnProperty("属性"),判断某个对象下面是否有某个属性,如果属性是原型对象里面的或者属性不存在,都会返回false。
最后,再看一段代码:
function Person (name) {
this.name = name;
}
Person.prototype.sex = 'boy';
Person.prototype.friendsArr = ['tom','lucy'];
Person.prototype.looksLikeObj = {
hair: "long black",
eyes: "blue"
};
Person.prototype.sayWords = function(des) {
console.log(des+' say words:', this.name,this.sex, this.looksLikeObj, this.friendsArr);
}
let p1 = new Person('p1');
p1.sayWords('p1-des1');
console.log("Person实例对象:p1(改变前):",p1);
console.log("Person原型对象:p1.__proto__:",Object.getPrototypeOf(p1));
console.log("----------------------")
p1.name = 'p1-1001';
p1.sex = 'girl';
p1.friendsArr.push('lilei');
p1.looksLikeObj['cloth'] = "skirt";
// p1.xyz['a'] = 'xyza'; // xyz对象不存在,Cannot set property 'a' of undefined
p1.sayWords('p1-des2');
console.log("Person实例对象:p1(改变后):",p1);
console.log("Person原型对象:p1.__proto__:",Object.getPrototypeOf(p1));
结果截图:
对比一下,如果通过实例对象设置一个引用类型的值,如果实例对象本身不存在该属性,而原型对象存在,它会直接修改共享的公共原型对象,从而影响到其他实例对象。
所以,除非你需要这样的效果,否则,尽量不要将引用类型的属性设置在原型对象中,可以考虑放到构造函数中设置引用类型的属性。