1.简单理解js对象
在了解原型链之前,我们先要弄清楚什么是JavaScript的对象,JavaScript对象又由哪些组成。有人说一个程序就是一个世界,那么我们可以把对象称之为这个世界的组成类型,可以是生物,植物,生活用品等等。我们在java中管这些类型叫做类,但是在JavaScript中没有类的说法,当然ES6新标准中开始出现了类。但是在此之前,我们都管这些类型叫做对象。那么对象创建出来的实例就是就是组成该世界的各个元素,如一个人、一只小狗、一棵树等等。这些就称之为对象的实例。那么每种类型都有它不同的属性和方法,同样的JavaScript对象也是由对象属性和对象方法组成。当然了每个实例还可以存在与对象不一样的方法与属性。
var person = {
name:"xiaoming", //对象属性
sayName:function(){ //对象方法
console.log(this.name);
}
}
2.js对象属性的特性
在JavaScript对象中,每个属性都有其各自的特性,比如你的性别具有不可修改的特性。那么下面简单粗略介绍一下这几个特性。这些特性在JavaScript中是不能直接访问的,特性是内部值。
- [[Configurable]]: 表示能不能删除重新定义属性,能不能修改属性等 默认true
- [[Enumerable]]: 表示能不能通过for-in遍历等 默认true
- [[Writeable]]: 表示能不能修改属性值 默认true
- [[Value]]: 表示属性的值,写入到这里,读从这里读 默认undefined
如果要修改属性的默认特性,可以使用Object.defineProperty()
方法,当然在这里就不再继续展开了。接下来我们开始介绍对象的创建
1.工厂模式
function createPerson(name,sex){
let obj = new Object();
obj.name = name;
obj.sex = sex;
obj.sayName = function(){
console.log(this.name);
}
return obj;
}
let p1 = new createPerson("小明","男");
这就是工厂模式,在函数内创建对象,然后在函数内封装好后返回该对象。但是该方法有个缺点就是看不出该对象的类型,于是乎构造函数模式应运而生。
2.构造函数模式
function Cat(name,color){
this.name = name;
this.color = color;
this.sayName = {
console.log("我是"+name+"猫");
}
}
let Tom = new Cat("Tom","灰白");
let HelloKity = new Cat("HelloKity","粉红");
构造函数模式和工厂模式的区别在于,构造函数模式没有用return
语句,直接把属性赋给了this
语句,并且没有显式的创建对象。当然,如果细心的朋友应该会发现函数名首字母大写了,这是约定在构造函数时将首字母大写。
用构造函数创建新实例时,必须要用new
操作符。同时,每个由构造函数创建的实例都会有一个constructor
指向该构造函数
Tom.constructor == Cat //true
这时候我们就会想一个问题,我们在创建不同的Cat
实例时,我们就会创建多个不同sayName
函数,但是他们执行的功能都是一样的,这时候我们就会想要一种更优化的方法。这时,我们需要引入原型属性(prototype)的概念了
3.原型模式
我们创建的每个函数里面都会有个prototype
属性,这个就是原型属性,这个属性是个指针,指向一个该函数的原型对象。我们可以捋一捋对象,对象原型,实例这三者的关系,简单来说,我们可以把对象想象成爸爸,那么对象原型就是爷爷,实例的话好比是儿子。爷爷有的东西(属性、方法),每个儿子都会遗传到的,当然如果爸爸把爷爷的东西修改了一下,那么到儿子手上的就是爸爸修改过的东西了(方法重写)。当然,儿子也算是爷爷骨肉嘛,那么儿子就会有个指针[[prototype]]
指向爷爷,在Chrome、Firefox等浏览器上面可以用属性__proto__
可以访问到。
-
那么
prototype
和__proto__
区别在哪?
这么说,简单的说prototype
是指向各自的爸爸,__proto__
是指向各自的爷爷。当然这说法只是为了更好理解这两者是有区别的。接下来我给大家做一个图让大家更好的理解这两者的区别。
这大概也是明白为什么对象实例存在个constructor
指针指向对象了,因为对象原型上面存在这个属性指向该对象,而且原型最初只包含该constructor
属性。而实例寻找属性值的时候会向上找,先在实例中搜索该属性,没有的话向对象原型寻找。所以最后找到并返回该值。这样就能很清楚的分开prototype
和__proto__
的区别了。prototype
是对象的属性,而__proto__
是对象实例的属性。
那么我们基本了解prototype
属性以后,我们就可以给大家说说原型模式了。
function Cat(){
}
Cat.prototype.name = "Tom";
Cat.prototype.color = "灰白";
Cat.prototype.sayName = function(){
console.log(this.name);
}
let cat1 = new Cat();
let cat2 = new Cat();
cat1.sayName(); //"Tom"
cat2.sayName(); //"Tom"
console.log(cat1.color); //"灰白"
console.log(cat2.color); //"灰白"
//因为对象原型是共享属性与方法,所以所有实例都可以访问到
//接下来玩点更复杂的
Cat.sayName = function(){
console.log("我是Cat");
}
cat1.sayName = function(){
console.log("我是cat1");
}
let cat3 = new Cat();
cat1.sayName(); //"我是cat1"
cat2.sayName(); //"Tom"
cat3.sayName(); //"Tom"
Cat.sayName(); //"我是Cat"
这时候很多人就懵了,为什么cat3
说的是"Tom"
,而不是输出"我是Cat"
。这是因为 Cat.sayName 这个函数是类方法,我们要注意一点,Cat
也是一个函数,函数就是一个对象,可以为其添加方法和属性。所以我们在实例中调用sayName并不是调用该类方法。我们还需要分清类方法与对象方法的区别。
function Person(){ //通过对象实例调用
this.say = function(){
console.log("我是Person对象方法");
}
}
Person.say = function(){ //只能通过Person调用
console.log("我是Person类方法");
}
Person.prototype.say = function(){ //通过对象实例调用
console.log("我是Person对象原型方法");
}
到这里,也许还是会有点懵,为什么后面的cat1.sayName(); //"我是cat1"
,因为对象实例方法会屏蔽掉原型的方法。我们之前说过,当代码读取对象的某个属性时,它会先从该对象实例开始搜索,如果找不到再往上搜索。所以当你定义了对象实例的方法时,如果跟对象原型中的同名,那么该对象实例的方法就会屏蔽掉对象原型中的方法。所以cat1
第二次输出的是我是cat1
。
到这里,我再总结一下对象原型,对象与对象实例之间的关系。
-
对象原型内的方法与属性可以供所有的对象实例访问,实现共享性。
-
对象的
prototype
属性可以找到对象原型,而对象实例的[[proto]]
可以找到对象原型 - 对象实例可以重写对象原型方法,使其屏蔽对象原型的方法
- 对象原型一开始只有
constructor
属性,该属性指向该对象 - 分清对象原型方法,对象方法,对象实例方法,类方法区别。类方法不需要通过实例化对象去访问,而其他的都要对象实例去访问
那么到这里我们已经弄懂了对象原型,对象与对象实例之间的关系。下面我再介绍一种简单的原型语法。
function Cat(){
}
Cat.prototype = {
name:"Tom",
color:"灰白",
sayName:function(){
console.log(this.name);
},
}
这样我就以字面量的形式创建了新对象,但是有个不一样的地方就是constructor
属性不指向Cat
,因为我们创建一个函数就会创建它的原型对象,原型对象里面自动获得constructor
属性,那么我们再这样的情况下,重写了整个原型对象。所以此时的constructor
属性指向了Object
。那么我们如果非要这个属性怎么办?很好办,我们自己给它加上就好。
function Cat(){
}
Cat.prototype = {
constructor:"Cat",
name:"Tom",
color:"灰白",
sayName:function(){
console.log(this.name);
},
}
最后我们讲一下原型模式的缺点,原型模式的缺点也很明显,就是它的共享性。成也共享败也共享。这让我突然想起共享单车。废话不多说,直接撸码上来
function Cat(){
}
Cat.prototype.name = "Tom";
Cat.prototype.color = "灰白";
Cat.prototype.catchMouse = ["Jerry"];
Cat.prototype.sayName = function(){
console.log(this.name);
}
let cat1 = new Cat();
let cat2 = new Cat();
cat1.catchMouse.push("Mickey");
console.log(cat1.catchMouse); //["Jerry", "Mickey"]
console.log(cat2.catchMouse); //["Jerry", "Mickey"]
因为原型上面的属性是所有实例都可以访问的,那么当添加往catchMouse
属性添加一个值时,所有实例皆可以访问到该属性。这就让人们很头疼了,每个实例的属性应该都是不一样的才对,起码正常来说,但是这样弄得大家都一样的话,就有问题了。这时候,聪明的人应该都可以想到,将构造函数模式和原型模式组合起来就可以了。
4.组合构造函数模式和原型模式
将其组合起来,结合他们两的优点,是普遍认同度最高的对象创建模式
function Cat(name,color){
this.name = name;
this.color = color;
this.catchMouse = [];
}
Cat.prototype.sayName = function(){
console.log(this.name);
}
let cat1 = new Cat("Tom","灰白");
let cat2 = new Cat("HellowKity","粉红");
cat1.catchMouse.push("Jerry");
cat1.sayName(); //"Tom"
cat2.sayName(); //"HellowKity"
console.log(cat1.catchMouse); //["Jerry"]
console.log(cat2.catchMouse); //[]
最后
本篇介绍了对象与对象的创建方法。同时引入并介绍了对象原型的概念。解析了对象原型,对象与对象实例间的关系。我们在下一篇将会解析原型链的概念以及对象的继承与原型链的关系,带大家敲开原型链的奥秘。
原创文章,转载请注明出处