JavaScript由浅及深了解原型链(一)

一.什么是js对象

1.简单理解js对象

在了解原型链以前,咱们先要弄清楚什么是JavaScript的对象,JavaScript对象又由哪些组成。有人说一个程序就是一个世界,那么咱们能够把对象称之为这个世界的组成类型,能够是生物,植物,生活用品等等。咱们在java中管这些类型叫作,可是在JavaScript中没有类的说法,固然ES6新标准中开始出现了类。可是在此以前,咱们都管这些类型叫作对象。那么对象建立出来的实例就是就是组成该世界的各个元素,如一我的、一只小狗、一棵树等等。这些就称之为对象的实例。那么每种类型都有它不一样的属性和方法,一样的JavaScript对象也是由对象属性和对象方法组成。固然了每一个实例还能够存在与对象不同的方法与属性。java

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()方法,固然在这里就再也不继续展开了。接下来咱们开始介绍对象的建立函数

二.建立JavaScript对象

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语句,而且没有显式的建立对象。固然,若是细心的朋友应该会发现函数名首字母大写了,这是约定在构造函数时将首字母大写。this

用构造函数建立新实例时,必需要用new操做符。同时,每一个由构造函数建立的实例都会有一个constructor指向该构造函数spa

Tom.constructor == Cat  //true

这时候咱们就会想一个问题,咱们在建立不一样的Cat实例时,咱们就会建立多个不一样sayName函数,可是他们执行的功能都是同样的,这时候咱们就会想要一种更优化的方法。这时,咱们须要引入原型属性(prototype)的概念了prototype

3.原型模式

咱们建立的每一个函数里面都会有个prototype属性,这个就是原型属性,这个属性是个指针,指向一个该函数的原型对象。咱们能够捋一捋对象,对象原型,实例这三者的关系,简单来讲,咱们能够把对象想象成爸爸,那么对象原型就是爷爷,实例的话比如是儿子。爷爷有的东西(属性、方法),每一个儿子都会遗传到的,固然若是爸爸把爷爷的东西修改了一下,那么到儿子手上的就是爸爸修改过的东西了(方法重写)。固然,儿子也算是爷爷骨肉嘛,那么儿子就会有个指针[[prototype]]指向爷爷,在Chrome、Firefox等浏览器上面能够用属性__proto__能够访问到。指针

  • 那么prototype__proto__区别在哪?

这么说,简单的说prototype是指向各自的爸爸,__proto__是指向各自的爷爷。固然这说法只是为了更好理解这二者是有区别的。接下来我给你们作一个图让你们更好的理解这二者的区别。code

未命名文件 (3).png

这大概也是明白为何对象实例存在个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);       //[]

最后

本篇介绍了对象与对象的建立方法。同时引入并介绍了对象原型的概念。解析了对象原型,对象与对象实例间的关系。咱们在下一篇将会解析原型链的概念以及对象的继承与原型链的关系,带你们敲开原型链的奥秘。

原创文章,转载请注明出处

相关文章
相关标签/搜索