原型、原型链和继承

原型:JavaScript 中,万物皆对象!但对象也是有区别的。分为普通对象和函数对象
原型的概念:每一个javascript对象创建的时候,就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型中“继承”属性。

构造函数

function Star(age,name) {
    this.age = age
    this.name = name
    this.sing = function(){
        console.log("我可以唱歌")
    }
}
let ldh = new Star(35,"刘德华")
console.log(ldh)//35,刘德华
console.log(Star.sing)//undefind|实例成员不能通过构造函数访问
Star.sex='男';
console.log(Star.sex)//男|静态成员只能通过构造函数访问
console.log(ldh.sex)//undefind|静态成员只能通过构造函数访问

构造函数里面的内容,构造函数里面是有静态成员和实例成员
实例成员就是构造函数内部通过this添加的成员:age,name,sing都是实例成员,不能通过构造函数访问
静态成员,就是直接在函数本身上面添加的成员,只能通过构造函数访问
内存浪费问题:当我们使用构造函数创建多个实例的时候,如果这个函数里面有多个实例成员的函数,那么我们每一次创建实例对象,构造函数下的方法都会重新开辟一个内存空间去存放这些方法。但是明明这些方法都是一个构造函数里面的,干嘛要这么麻烦,还要占用内存,所以出现了原型。

一:prototype

在JavaScript中,每一个构造函数都有一个 prototype 属性,该属性指向函数的原型对象。
我们可以把那些不变的方法,直接定义在prototype对象上这样所有的实例都可以共享这个方法

function Star(age,name) {
    this.age = age
    this.name = name
}
Star.prototype.sing = function(){
    console.log("我会唱歌")
}
let ldh = new Star(35,"刘德华")
ldh.sing//我会唱歌

因为prototype是一个对象,所以我们称构造函数的prototype为原型对象

function Person(){}
Person.prototype.name = "protoName"
var person = new Person()
console.log(person.name) // protoName

解析:person对象没有name属性,他将去他的构造函数(Person)里面去找,构造函数里面没有,就去构造函数的原型(Person.prototype)里面找,如果没有找到返回值为undefined。


image.png

二:proto[注解:是两个_,而不是一个]

每一个对象都有一个proto属性,指向构造函数的prototype原型对象,他指向该对象的原型。

function Person(){}
var person = new Person()
console.log(person.__proto__ === Person.prototype) // true
image.png

三:constructor(构造函数)

每个原型都有一个constructor属性(也就是说,constructor是存在于prototype上面的属性),指向该关联的构造函数。

function Star(){}
var ldh = new Star()
console.log(Star.prototype.constructor === Star) // true
console.log(Star.prototype.constructor === ldh.constructor) // true
console.log(ldh.__proto__.constructor === Star.prototype.constructor) // true

解析:ldh没有constructor属性 -> 查找Star的constructor属性,Star没有constructor属性 -> 查找Star.prototype的constructor属性


image.png
function Star(age, name) {
    this.age = age
    this.name = name
}
Star.prototype = {
    constructor:Star,//
    sing:function(){
        console.log("我会唱歌")
    },
    movie:function(){
        console.log("我会演电影")
    }
}
let ldh = new Star(35, "刘德华")
console.log(ldh.__proto__.constructor)//function Object() { [native code] }

解析:为何不是Star,Star.prototype本身就是一个对象,你给Star.prototype赋值一个对象,那就会把原来prototype上面的对象给覆盖了,所以这个时候就可以使用constructor了
整合:

    function Animal(name) {
            this.name= "animal";
            this.age= 18;
            this.setName= function (name) {
                this.name = name
            }
        }
 Animal.prototype.setAge =function(age){
            this.age = age
        }
 var dog = new Animal("dog")
 var cat = new Animal("cat")
image.png

Animal是构造函数,dog、cat是构造函数创建的对象


image.png

继承:JavaScript 只有一种结构:对象。每个实例对象(object)都有一个私有属性(称之为 proto )指向它的构造函数的原型对象(prototype)。该原型对象也有一个自己的原型对象(proto),层层向上直到一个对象的原型对象为 null。null 没有原型,并作为这个原型链中的最后一个环节。
给dosomething的原型加上属性

function doSomething(){}
doSomething.prototype.foo = "bar";
console.log( doSomething.prototype );

运行结果

{
    constructor: ƒ doSomething(),
    foo: "bar",
    __proto__: {
        constructor: ƒ Object(),
        hasOwnProperty: ƒ hasOwnProperty(),
        isPrototypeOf: ƒ isPrototypeOf(),
        propertyIsEnumerable: ƒ propertyIsEnumerable(),
        toLocaleString: ƒ toLocaleString(),
        toString: ƒ toString(),
        valueOf: ƒ valueOf()
    }
}

原型链

image.png

1:首先说好了的每一个实例都有proto,而这个就指向的是构造函数的对象原型,所以ldh.proto和Star.prototype是等价的,即ldh.proto`能够只会Star这个构造函数呢,本质上就是通过Star.prototype指回去的。
2:那么我们就会想到Star.prototype的proto指向的会是谁呢,一打印就能看的出指向的是Object.prototype,所以不用想也知道他的构造函数肯定是Object了
3:那Object.prototype还有没有proto,打印一看就知道是为null,所以我们知道原型的终点就是null

js的成员查找机制

当访问一个对象的属性或者方法的时候,首先查找这个对象自身有没有这个属性,如果没有就去找他的原型,也就是proto指向的prototype原型对象,如果还没有就去查找原型对象的原型,即Object的原型对象以此类推一直找到null为止

javascript是面向对象编程思想,面向对象思想有三大特性:封装、继承、多态。

封装

es5没有class概念,类就是一个函数有自己的方法和属性,将方法和属性组成一个类的过程就是封装
封装有3种:构造函数、prototype、.
1:构造函数
·首字母大写
·内部使用this
·使用new生成实例

  function Cat(name,color){
    this.name = name;
    this.color = color;
       this.eat = function(){alert("吃老鼠")};
  }

缺点:通过this定义的属性和方法,我们实例化对象的时候都会内存中重新复制一份,造成内存的浪费。
2:prototype

  function Cat(name,color){
    this.name = name;
    this.color = color;
  }
 Cat.prototype.type = "猫科动物";
 Cat.prototype.eat = function(){alert("吃老鼠")};

优点:所有的实例的type,eat方法指向的同一个内存地址,提高了效率。
缺点:编写麻烦
3:.

  function Cat(){}
 Cat.type = "猫科动物";
 Cat.eat = function(){alert("吃老鼠")};
var blueCat=new Cat();
console.log(Cat.type)//猫科动物
console.log(blueCat.type)//undefined

缺点:只能通过该类访问

javascript也有private public protected

var Book = function (id, name, price) {
        //private(在函数内部定义,函数外部访问不到,实例化之后实例化的对象访问不到)
        var num = 1;
        var id = id;
        function checkId() {
            console.log('private')
        }
        //protected(可以访问到函数内部的私有属性和私有方法,在实例化之后就可以对实例化的类进行初始化拿到函数的私有属性)
        this.getName = function () {
            console.log(id)
        }
        this.getPrice = function () {
            console.log(price)
        }
        //public(实例化的之后,实例化的对象就可以访问到了~)
        this.name = name;
        this.copy = function () {
            console.log('this is public')
        }
    }
    //在Book的原型上添加的方法实例化之后可以被实例化对象继承
    Book.prototype.proFunction = function () {
        console.log('this is proFunction')
    }
    //在函数外部通过.语法创建的属性和方法,只能通过该类访问,实例化对象访问不到
    Book.setTime = function () {
        console.log('this is new time')
    }
    var book1 = new Book('111','悲惨世界','$99')
    book1.getName();        // 111 getName是protected,可以访问到类的私有属性,所以实例化之后也可以访问到函数的私有属性
    book1.checkId();        //报错book1.checkId is not a function
    console.log(book1.id)   // undefined id是在函数内部通过定义的,是私有属性,所以实例化对象访问不到
    console.log(book1.name) //name 是通过this创建的,所以在实例化的时候会在book1中复制一遍name属性,所以可以访问到
    book1.copy()            //this is public
    book1.proFunction();    //this is proFunction
    Book.setTime();         //this is new time
    book1.setTime();        //报错book1.setTime is not a function

new的实质

var o = new Object();
1:新建一个对象o
2:o. proto = Object.prototype 将新创建的对象的proto属性指向构造函数的prototype
3:将this指向新创建的对象
4:返回新对象,但是这里需要看构造函数有没有返回值,如果构造函数的返回值为基本数据类型string,boolean,number,null,undefined,那么就返回新对象,如果构造函数的返回值为对象类型,那么就返回这个对象类型

继承

js继承有8类:原型链继承、借用构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合式继承、混入方式继承多个对象、ES6类继承extends
1:原型链继承[SubType.prototype = new SuperType(); ]

function SuperType(){
  this.property = true;
  this.colors = ["red", "blue", "green"];
}
function SubType(){
  this.subproperty = false;
}
// 这里是关键,创建SuperType的实例,并将该实例赋值给SubType.prototype
SubType.prototype = new SuperType();
var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
var instance2 = new SubType(); 
alert(instance2.colors); //"red,blue,green,black"

缺点:原型链方案存在的缺点:多个实例对引用类型的操作会被篡改。


image.png

2:借用构造函数继承[子类里面SuperType.call(this);]

function  SuperType(){
    this.color=["red","green","blue"];
}
function  SubType(){
    //继承自SuperType
    SuperType.call(this);
}
var instance1 = new SubType();
instance1.color.push("black");
alert(instance1.color);//"red,green,blue,black"

var instance2 = new SubType();
alert(instance2.color);//"red,green,blue"

核心代码是SuperType.call(this),创建子类实例时调用SuperType构造函数,于是SubType的每个实例都会将SuperType中的属性复制一份。
缺点:
只能继承父类的实例属性和方法,不能继承原型属性/方法,无法实现复用,每个子类都有父类实例函数的副本,影响性能
3:组合继承

// 组合继承: 借用构造函数继承 + 原型链继承 一起使用
// 借用构造函数继承 :可以继承构造函数的属性
// 原型链继承 :可以继承原型链的成员
function SuperType(name){
  this.name = name;
  this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
  alert(this.name);
};
function SubType(name, age){
  // 继承属性, 构造函数继承,第二次调用SuperType()
  SuperType.call(this, name);
  this.age = age;
}
// 继承方法,构建原型链继承,第一次调用SuperType()
SubType.prototype = new SuperType(); 
// 重写SubType.prototype的constructor属性,指向自己的构造函数SubType
SubType.prototype.constructor = SubType; 
SubType.prototype.sayAge = function(){
    alert(this.age);
};
var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29
//
var instance2 = new SubType("Greg", 27);
alert(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27

缺点:
第一次调用SuperType():给SubType.prototype写入两个属性name,color。
第二次调用SuperType():给instance1写入两个属性name,color。
实例对象instance1上的两个属性就屏蔽了其原型对象SubType.prototype的两个同名属性。所以,组合模式的缺点就是在使用子类创建实例对象时,其原型中会存在两份相同的属性/方法。
4:原型式继承
利用一个空对象作为中介,将某个对象直接赋值给空对象构造函数的原型。object()对传入其中的对象执行了一次浅复制,将构造函数F的原型直接指向传入的对象。

function object(obj){
  function F(){}
  F.prototype = obj;
  return new F();
}
var person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
alert(person.friends);   //"Shelby,Court,Van,Rob,Barbie"

缺点:
原型链继承多个实例的引用类型属性指向相同,存在篡改的可能。
无法传递参数
另外,ES5中存在Object.create()的方法,能够代替上面的object方法。
5:寄生式继承

function object(obj){
  function F(){}
  F.prototype = obj;
  return new F();
}
function createAnother(original){
  var clone = object(original); // 通过调用 object() 函数创建一个新对象
  //var clone = Object.create(original); es5中和上面等价
  clone.sayHi = function(){  // 以某种方式来增强对象
    alert("hi");
  };
  return clone; // 返回这个对象
}
var person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //"hi"

缺点(同原型式继承):
原型链继承多个实例的引用类型属性指向相同,存在篡改的可能。
无法传递参数
6:寄生组合式继承
结合借用构造函数传递参数和寄生模式实现继承

function inheritPrototype(subType, superType){
  var prototype = Object.create(superType.prototype); // 创建对象,创建父类原型的一个副本
  prototype.constructor = subType;                    // 增强对象,弥补因重写原型而失去的默认的constructor 属性
  subType.prototype = prototype;                      // 指定对象,将新创建的对象赋值给子类的原型
}
// 父类初始化实例属性和原型属性
function SuperType(name){
  this.name = name;
  this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
  alert(this.name);
};
// 借用构造函数传递增强子类实例属性(支持传参和避免篡改)
function SubType(name, age){
  SuperType.call(this, name);
  this.age = age;
}
// 将父类原型指向子类
inheritPrototype(SubType, SuperType);
// 新增子类原型属性
SubType.prototype.sayAge = function(){
  alert(this.age);
}
var instance1 = new SubType("xyc", 23);
var instance2 = new SubType("lxy", 23);
instance1.colors.push("2"); // ["red", "blue", "green", "2"]
instance1.colors.push("3"); // ["red", "blue", "green", "3"]

这个例子的高效率体现在它只调用了一次SuperType 构造函数,并且因此避免了在SubType.prototype 上创建不必要的、多余的属性。于此同时,原型链还能保持不变;因此,还能够正常使用instanceof 和isPrototypeOf()
这是最成熟的方法,也是现在库实现的方法
7:混入方式继承多个对象

function MyClass() {
     SuperClass.call(this);
     OtherSuperClass.call(this);
}
// 继承一个类
MyClass.prototype = Object.create(SuperClass.prototype);
// 混合其它
Object.assign(MyClass.prototype, OtherSuperClass.prototype);
// 重新指定constructor
MyClass.prototype.constructor = MyClass;
MyClass.prototype.myMethod = function() {
     // do something
};

Object.assign会把 OtherSuperClass原型上的函数拷贝到 MyClass原型上,使 MyClass 的所有实例都可用 OtherSuperClass 的方法。
8:ES6类继承extends
extends关键字主要用于类声明或者类表达式中,以创建一个类,该类是另一个类的子类。其中constructor表示构造函数,一个类中只能有一个构造函数,有多个会报出SyntaxError错误,如果没有显式指定构造方法,则会添加默认的 constructor方法,使用例子如下。

class Rectangle {
    // constructor
    constructor(height, width) {
        this.height = height;
        this.width = width;
    }
    // Getter
    get area() {
        return this.calcArea()
    }
    // Method
    calcArea() {
        return this.height * this.width;
    }
}
const rectangle = new Rectangle(10, 20);
console.log(rectangle.area);
// 输出 200
-----------------------------------------------------------------
// 继承
class Square extends Rectangle {
  constructor(length) {
    super(length, length);
    // 如果子类中存在构造函数,则需要在使用“this”之前首先调用 super()。
    this.name = 'Square';
  }
  get area() {
    return this.height * this.width;
  }
}
const square = new Square(10);
console.log(square.area);
// 输出 100

extends继承的核心代码如下,其实现和上述的寄生组合式继承方式一样

function _inherits(subType, superType) {
    // 创建对象,创建父类原型的一个副本
    // 增强对象,弥补因重写原型而失去的默认的constructor 属性
    // 指定对象,将新创建的对象赋值给子类的原型
    subType.prototype = Object.create(superType && superType.prototype, {
        constructor: {
            value: subType,
            enumerable: false,
            writable: true,
            configurable: true
        }
    });
    if (superType) {
        Object.setPrototypeOf 
            ? Object.setPrototypeOf(subType, superType) 
            : subType.__proto__ = superType;
    }
}

来源:https://juejin.cn/post/6844903696111763470
https://juejin.cn/post/6844903480868470798

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容