Go to comments

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 ,还是浅粉色字代表是系统自己设置隐式的

image.png


给 Car.prototype 里加一个我们设置的的属性就不是浅粉色了

Car.prototype.abc = 123; // 我们自己设置的属性abc

function Car(color, owner){

}

var DiDi = new Car();

console.log(Car.prototype);

我们设置的属性abc是深紫色

image.png

在原型的内部一开始自带的 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);

但在控制台点开三角号,会把里面的真实属性展示出来

图片.png


会发现里面有一个  __proto__  属性,并且是浅粉色代表系统自定义的属性,后面跟了一个 Object 也是对象还可以点开

图片.png


点开发现里面

图片.png

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__  返回一个对象

图片.png

打开对象里面有  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

***/



Leave a comment 0 Comments.

Leave a Reply

换一张