JavaScript 原型
原型
1. 定义:
原型是function对象的一个属性,它定义了构造函数制造出的对象的公共祖先,
通过该构造函数产生的对象,可以继承该原型的属性和方法。原型也是对象
2. 利用原型特点和概念,可以提取共有属性
3. 对象属性的增删和原型上属性增删改查
4. 对象如何查看原型 —> 隐式属性 __proto__
5. 对象如何查看对象的构造函数 —> constructor
对象是js里面的一大块,今后对对象的应用五花八门的非常多非常灵活,
原型也是对象的一部分知识点,上节课讲的是对象的构造工厂、对象怎么来的,这节课讲对象怎么来的,然后在来的基础上还能更美好的来。
先铺垫一点,
编程是人类根据自己的思想总结出一套规律,然后放到程序里让我们去学,一切的东西都是基于生活的一些真实的模板,这个原型也是,
原型换在咱们生活中,比如把人类当做一个对象,原型就是人类的祖先,人类的祖先给后代人类留下了什么东西呢?
比如相貌体征DNA都是遗传来的,
比如姓氏都是遗传来的,包括有的同学的父亲是音乐家,孩子生下来之后就带有点音乐细胞,这些都是遗传来的。
原型对于对象来说就是一个祖先的作用,祖先提供一些属性和方法可供它的后代去继承,这个继承和生活中的继承还有那么一点点的区别。
一、原型的定义
先看原型的定义:
原型是function对象的一个属性,它定义了构造函数制造出的对象的公共祖先。
函数名Person代表函数体(代表函数引用),函数也相当于对象,是对象就有属性和方法,
现在给函数加一个 prototype 属性,这是系统自带的属性,prototype 翻译过来就是这节课讲的原型。
// prototype 在函数刚刚出生的时候就已经被定义好了,不是人为写的,这里写出来只是为了形象化 // Person.prototype // 原型 Person.prototype = {} // 是祖先 function Person(){ } var Li = new Person();
prototype 在函数刚刚出生的时候就已经被定义好了,他能干什么呢?
首先他是一个对象,可以理解为是个空对象 {} ,这个空对象是个什么东西呢?它就相当于“构造函数Person”构造出对象的爹。
祖先是一个空对象,给这个空对象加一个name属性,现在祖先上就有一个name属性,对象 Li 是祖先的晚辈,晚辈就能继承这个属性
Person.prototype.name = "hehe"; function Person(){ } var Li = new Person(); console.log(Li);// Person {} 对象Li里面是空的没有name属性 console.log(Li.name); // 然后Li对象调动name属性输出hehe
name属性没在对象Li身上,但是Li的祖先有name属性,对象Li调用祖先的name属性是一种继承,原型描述的就是这种继承的关系
再看原型的定义:
原型是function对象的一个属性,它定义了构造函数制造出的对象的公共祖先。
什么叫公有祖先?
Person构造出的两个对象 Li 和 Glee,
两个对象有一个共同的祖先 Person.prototype ,继承自上面有的属性,两个对象都可以用
Person.prototype.LastName = "Fang"; function Person(){ } var Li = new Person(); var Glee = new Person(); console.log(Li.LastName); // Fang console.log(Glee.LastName); // Fang
两个对象(Li 和 Glee)访问LastName属性都能输出Fang,原型是该构造函数构造出的对象的公有祖先
二、利用原型特点和概念,可以提取共有属性
给原型加一个say()方法,生成的对象都可以用say()方法
Person.prototype.LastName = "Fang"; Person.prototype.say = function(){ // 给原型一个say()方法 console.log('hehe'); } function Person(){ } var Li = new Person(); var Glee = new Person(); Glee.say(); // hehe Li.say(); // hehe
两个对象(Li、Glee)都上没有LastName属性和say方法,用的是原型上面的,叫借用是一种继承关系,并不真正的属于这两个对象自己
当构造函数自己有了 LastName 属性以后,现在对象上、原型上都有 LastName 属性,访问对象时取谁的属性值?
取对象自己的,可近的来,没有再往原型上找
Person.prototype.LastName = "Fang"; Person.prototype.say = function(){ console.log('hehe'); } function Person(){ this.LastName = "Jing"; // 构造函数自己的LastName属性 } var Li = new Person(); var Glee = new Person(); console.log(Glee.LastName); // Jing console.log(Li.LastName); // Jing
写一个比较生动拟人化的,名字是自己的,姓是父亲的
Person.prototype.LastName = "Fang"; // 姓是父亲的 Person.prototype.say = function(){ console.log('hehe'); } function Person(name, age, sex){ this.name = name; // 名字是自己的 this.age = age; this.sex = sex; } var Glee = new Person('LiLi', '37', 'female'); // 生成的对象有自己的属性,也有继承原型的属性 console.log(Glee.name); // 访问自己有的name属性输出Li console.log(Glee.LastName); // 访问自己没有的属性,继承原型的属性也相当于访问自己的属性输出Fang
上面用拟人化方法来理解,原型就像一个公有祖先,可以去继承祖先的东西,工业化的原型是怎么使用的呢?
比如,Car是一个造汽车的构造工厂,汽车车主的名字、汽车的颜色可以选配( 生产的汽车有固定的也有可以选配的 )
function Car(color, owner){ this.owner = owner; // 车主的名字,是可选的 this.carName = "BMW"; this.height = 1400; this.lang = 4900; this.color = color; // 汽车的颜色,是可选的 } var DiDi = new Car('red', 'Ming');
现在来看就有点问题,
构造函数里面流程化生产的 this.carName="BMW , this.height=1400 , this.lang=4900 ,每创建一个对象要执行一次,这叫是代码的冗余、代码的耦合
每一次都是一模一样的东西,应该采取一个方法提取出来,可以通过继承的规则,把这些一模一样的放的原型里面去
// 现在这三个行只执行一遍,因为原型只有一个 Car.prototype.carName = "BMW"; Car.prototype.height = 1400; Car.prototype.lang = 4900; function Car(color, owner){ this.owner = owner; this.color = color; } // 生产出来两辆车 var DiDi = new Car('red', 'Ming'); var DiDi1 = new Car('red', 'Li'); console.log(DiDi.carName); // BMW console.log(DiDi1.carName); // BMW
原型的第一点应用:
把工厂里生产的零件,把公有部分也就是说一模一样的不需要选配部分,提取出来放到原型里面。
三、对象属性的增删和原型上属性增删改查
1. 查看原型上属性
Person.prototype.LastName = "Fang"; function Person(name){ this.name = name; } var Li = new Person('li'); console.log(Li.LastName); // 输出Fang,对象Li自己没有LastName属性,但是能访问原型上的LastName属性,如果对象Li自己有LastName属性一定是可近的来访问自己的
2. 修改原型上的属性
Person.prototype.LastName = 'Fang'; function Person(name){ this.name = name; } var Li = new Person('li'); Li.LastName = 'Jing'; // 对象 Li 修改原型上的 lastName 属性,原来值是Fang,现在改为Jing(修改后控制台console上返回Jing) console.log(Li); // 修改后对象Li上多一个LastName属性 Person {name: "li", LastName: "Jing"} console.log(Li.LastName); // 访问Li.LastName属性返回Jing,但访问的是对象自己的属性
要想修改原型的属性除非是调用 Person.prototype.LastName = 'xxx'; ,否则通过对象修改原型基本是不可能的,
这样写 Li.LastName = 'Jing'; 不是修改原型属性,是给对象自己增加属性了。
3. 对象能给原型增加属性吗?
比如给原型增加一个 car 属性,这样 Person.prototype.car='奇瑞QQ'; 通过prototype是可以给原型增加属性,
但是想通过构造函数构造出的对象增加属性根本不可能,修改都不可能,增加就更别提了
Person.prototype.LastName = "Fang"; function Person(name){ this.name = name; } var Li = new Person('li'); Person.prototype.car = '奇瑞QQ'; // 给原型增加属性car console.log(Li.car); // 奇瑞QQ
4. 删除原型上的属性
Person.prototype.LastName = "Fang"; function Person(name){ this.name = name; } var Li = new Person('li'); console.log(delete Li.name); // 对象Li可以删除自己的属性,name属性删除后返回true console.log(Li.name); // 再访问name属性返回undefined,这是对象的一个特点,可以删除属性 console.log(delete Li.LastName); // 同样用delete操作符,删除原型上的LastName属性返回true console.log(Li.LastName); // 但是在访问LastName时,还是能输出属性值Fang
既然删不了怎么返回true?
比如删除一个没有的属性abc也返回true,因为电脑是白痴删除,没有的属性电脑说可以啊,所以就返回true
console.log(delete Li.abc); // true
5. 原型还可以有另外一种设置方法
原型是构造函数构造出的对象公有的祖先,既然是公有祖先就可以随意操作原型,原型本来就是一对象
造车车间的公有属性写到原型里,每一个属性都写一行 Car.prototype = "xxx"
Car.prototype.carName = "BMW"; Car.prototype.height = 1400; Car.prototype.lang = 4900; function Car(color, owner){ this.owner = owner; this.color = color; }
原型的每一个属性都要写一行,有没有更简单的写法?
Car.prototype 本来就是一对象(近乎是一个空对象),就直接给 Car.prototype 一个空对象对象,在空对象里这样写更简便一些
Car.prototype = { carName : "BMW", height : 1400, lang : 4900 } function Car(color, owner){ this.owner = owner; this.color = color; } var DiDi = new Car('red', 'LiLi'); console.log(DiDi.lang); // 对象DiDi可以继承原型的属性输出4900
四、对象如何查看对象的构造函数 -> constructor
把下面代码中的原型的部分都注释掉,注释掉也是有原型的,只不过 原型 类似于一个空对象,
构造方法的 prototype 属性是函数自身就有的,我们只不过在他基础上加了些东西,现在就想看看 prototype 里面原有的是什么东西,是不是一个纯的空对象?
1. 对象DiDi会继承原型,现在DiDi自己身上什么属性都没有,
2. DiDi调动 constructor 属性(构造器的意思)返回的是构造这个对象的构造函数 ƒ Car(){ }
// Car.prototype = { // carName : "BMW", // height : 1400, // lang : 4900 // } function Car(){ } var DiDi = new Car(); console.log(DiDi.constructor); // ƒ Car(){ }
对象DiDi身上没设置属性, constructor 属性应该是继承来的,既然是继承来的,看看 Car.prototype
function Car(){ } var DiDi = new Car(); console.log( Car.prototype );
Car.prototype 是一个空对象,展开空对象里第一条属性是 constructor ,还是浅粉色字代表是系统自己设置隐式的
给 Car.prototype 里加一个我们设置的的属性就不是浅粉色了
Car.prototype.abc = 123; // 我们自己设置的属性abc function Car(color, owner){ } var DiDi = new Car(); console.log(Car.prototype);
我们设置的属性abc是深紫色
在原型的内部一开始自带的 constructor 属性,指向的是构造函数Car,目的就是让Car构造出所有的对象,可以找到自己的构造器
虽然 constructor 是系统隐式的,但是我们可以手动更改的
function Person(){ } Car.prototype = { constructor: Person // 把系统隐式的"constructor:car",修改成我们的"constructor:Person" } function Car(){ } var DiDi = new Car(); console.log(DiDi.constructor); // ƒ Person(){}
五、对象如何查看原型 -> 隐式属性__proto__
这个 __proto__ 属性干什么用的?
构造出一个对象 Li 出来,Li上面什么属性都没有(原型里面就有个constructor属性)
function Person(){ } var Li = new Person(); // console.log(Li);
但在控制台点开三角号,会把里面的真实属性展示出来
会发现里面有一个 __proto__ 属性,并且是浅粉色代表系统自定义的属性,后面跟了一个 Object 也是对象还可以点开
点开发现里面
1. 有一个 constructor 指向构造函数 ƒ Person()
2. 还有一个 __proto__ 这是什么呢?
我们写的清楚点,给原型加一个name属性,然后把 __proto__ 属性打印出来
Person.prototype.name = "abc"; // 给原型加name属性 function Person(){ } var Li = new Person(); console.log(Li.__proto__); // 把对象Li.__proto__属性打出来
访问 Li.__proto__ 返回一个对象
打开对象里面有 name: abc 我们只给原型加的name属性了,也就是说 __proto__ 里面放的就是原型
ps:
__proto__这种前两杠后两杠是隐式的命名规则是系统规定的,系统说尽量别给修改啊,这是系统内部的,
如果在开发的时候,不希望同事在使用方法的时候访问这个属性,给这个属性开头加一个下划线_private 代表私人的,通过命名去规定,js没有绝对隐式的属性,可以通过命名去规定
function Person(){ var _private }
这个__proto__到底那来的呢,或者说 __proto__ 里面放的原型有什么用呢?
1. 其实是这样的,在我们用 new 构造对象的时候,构造函数里面发生了隐式三段式
2. 第一步在构造函数里面 var this = {} 空对象,
3. 这个对象里面其实并不是空的,里面是有东西,这东西就是 __proto__ 属性指向 Person.prototype
Person.prototype.name = "hehe"; function Person(){ // new构造函数,隐式第一步this等于一个空对象,对象里面不是空的,里面有__proto__属性指向Person.prototype // var this = { // __proto__: Person.prototype // } } new Person();
这个__proto__有什么用?
1. 当我们访问一个对象的属性时,如果对象身上没有这个属性
2. 系统会通过 __proto__ 指向的索引,去找 Person.prototype 上面有没有想要的属性,
3. __proto__相当于一个链接的关系,把原型 和 构造出的对象链接在一起了
对象 Li 访问 name 会先找对象自己的属性,如果自己name属性没有就会沿着 __proto__ 的指向去到 Person.prototype 上面去找name属性
Person.prototype.name = "abc"; function Person(){ // var this = { // __proto__: Person.prototype // } } var Li = new Person(); console.log(Li.name); // 返回的abc是原型上面的
__proto__ 里面存的是对象的原型,每个对象都有一个 __proto__ 属性指向它的原型,如果把这个对象的原型更改一下呢?
Person.prototype.name = "Fang"; function Person(){ // var this = { // __proto__: Person.prototype // } } var obj = { name : "Sunny" } var Li = new Person(); console.log(Li.__proto__); // Li原型是{name: "Fang", constructor: ƒ} Li.__proto__ = obj; // 现在把Li的原型改成obj对象 console.log(Li.name); // 现在Li访问name打印输出"Sunny",因为Li的原型改了
构造函数 Porson 构造出对象的原型,未必非得是 Person.prototype,它是可以被修改的
1. 一开始通过 new 构造这个对象的时候,会隐式的 var this = {} 对象,这个对象里面有一个__proto__属性指向Person.prototype
2. 所以在Li访问 Li.name 的时候,会先上对象自己上找name属性,如果没有它会沿着__proto__到对象的原型里面去找,
能找到原型的根本原因在于__proto__属性,如果改变这个__proto__指向,就会沿着__proto__指向别的地方了。
3. 现在改变__proto__的指向,反正对象 Li 就是构造函数内隐式的this,把Li的__proto__改成obj,自此后Li继承的对象变成obj了,
所以对象Li自己没有name属性就到prototype上去找就找到obj了,obj上有name属性就打印出sunny
4. 自此之后对象Li和原来的原型就没有关系了(换了个爹),这是__proto__的一个基本应用。
再看一个问题
Person.prototype.name = "sunny"; function Person(){ } var Li = new Person(); console.log(Li.name);// 对象Li访问name属性,输出的是sunny Person.prototype.name = "cherry"; // 修改原型上的name属性值 console.log(Li.name); // 现在访问Li.name输出的是cherry
1. 对象Li查找Li.name属性,Li自己身上没有,没有找自己身上的__proto__
2. __proto__指向的是Person.Prototype
3. 现在给Person.prototype.name改了一个值,访问的必然修改了
Person.prototype.name=cherry 写在new操作符合上边,打印输出的是什么?
Person.prototype.name = "sunny"; function Person(){ } Person.prototype.name = "cherry"; // 写new操作符上面 var Li = new Person(); console.log(Li.name); // 打印结果返回cherry
改变Person.prototype.name的值,写new下面和写new上面没有区别,这不是最难的最难的在下面的
换一个比较绕写法
Person.prototype.name = "sunny"; function Person(){ } var Li = new Person(); Person.prototype = { name : "cherry" } console.log(Li.name); // sunny console.log(Li.__proto__);
上面的写法和 Person.prototype.name = cherry 的写法不一样,
Person.prototype.name = cherry 的写法是在原有基础上把属性值改了,上面的写法是把原型给改了换了个新对象
换一种简单的写法,道理是类似的,容易理解
var obj = {name : "a"}; // 引用值obj var obj1 = obj; // obj和obj1指向的是同一个房间 obj = {name : "b"}; // obj等于一个新对象 console.log(obj1); // 打印obj1返回 {name: "a"}
这个只不过是复杂点
Person.prototype.name = "sunny"; function Person(){ // var this = {__proto__ : Person.prototype} } var Li = new Person(); Person.prototype = { // Person.prototype把空间换了,li__proto__指向的还是原来的Person.prototype指向的那个空间,原来那个空间的name值是sunny name : "cherry" } console.log(Li.name); // sunny
就是把这三个过程复杂点
1. 在new的时候发生了一个过程 var this = {} 类似于空对象的对象,里面存了一个__proto__指向Person.prototype
然后对象Li就是 __proto__ : Person.prototype,返回给对象Li
2. Person.prototype是一个名,真正的是这个名指向的空间
__proto__和Person.prototype指向的是一个空间
3. 然后Person.prototype要换一个空间,Person.prototype把自己指向的空间换了,
__proto__指向的空间没换,__proto__指向的还是原来Person.prototype指向的那个空间,原来的空间里name属性是值sunny
var obj = {name : "a"}; // 引用值obj var obj1 = obj; // obj和obj1指向的是同一个房间 obj = {name : "b"}; // obj换了一个空间 console.log(obj1); // 返回{name: "a"} Person.prototype = {name : "a"}; Li.__proto__ = Person.prototype; Person.prototype = {name : "b"}; // 这样理解打印Person.prototype和Li.__proto__指向的空间不是一个空间了 console.log(Person.prototype); console.log(Li.__proto__); // "Person.prototype.name = cherry"改的是房间里的属性, // 下面这样是把房间给改了,换了一个房间 Person.prototype = { name: "cherry" , }
所以和 Person.prototype.name = cherry 这个写法不是一回事,为什么不是一回事
再看一个例子,
还是 Person.prototype = {name : "cherry"} 换一个位置放到new操作符上面(写上面写下面是不一样的)
Person.prototype.name = "sunny"; function Person(){ // var this = {__proto__: Person.prototype} } Person.prototype = { // 放的new上面 name: "cherry" } var Li = new Person(); console.log(Li.name); // cherry /*** 考虑预编译,程序执行顺序 1.函数Person提升到上面 2.然后执行 Person.prototype.name = "sunny"; 3.然后执行 Person.prototype = { name : "cherry" } 4.在然后发生了new操作 发生var this = {__proto__ : Person.prototype}的变化 1.函数提升到上面 function Person(){ // var this = {__proto__ : Person.prototype} } 2.然后执行 Person.prototype.name = "sunny"; 3.在然后执行 Person.prototype = { name : "cherry" } 4.在然后执行到这个,发生了new操作,有new操作的函数内部才会发送隐式三步(3把2给覆盖了后,才发生的new操作) var Li = new Person(); console.log(Lu.name); // 返回cherry ***/