JS原型、原型链和7种继承方法【白话文讲解】

前言

        在学习JS原型、原型链和继承之前,我们必须先弄懂三个W,也就是我们常说的“学习三问”

            学习三问:

                1.它是什么?(What)

                2. 为什么用它?(Why)

                3. 什么时候用它?(When)       

        带着这三个问题去思考下面将要所学的知识,相信你一定会明白今后所学的每个知识点!!

友情提醒:学会某个知识点后一定要反复多敲几遍相关知识的代码,看懂和敲懂真的是两回事!

正文

小编先在这里简单概述一下构造函数

构造函数

                function Person(name,age){
                    this.name=name;
                    this.age=age;
                    this.sex='男';
                    return this;//每个函数都会有个默认return返回值属性,这里不写return意思就是默认返回值是this
                }
                var person = new Person('xiaoming'18);//创建一个实例对象
                var person2= new Person('xiaohong',16)//创建多个实例对象

大写字母开头一般都是构造函数:function Person(){}

new的过程:

        1.var person = new Person('a',123)=>将参数传进去,函数中的 this 会变成空对象

        2.this.name = name;this.age = age;this.sex= '男' 为赋值;return this 为实际的运行机制

        3.return 之后赋值给 person,person具备了 person.name = xiaoming、person.age = 18、person.sex= '男'

        4. 继续往下执行person2其属性值也是一样根据实参传递 

new总结: 

         1. 在构造函数中创建一个空对象

         2. 函数的this指向空对象,并且将这个对象的隐式原型(__proto__)指向构造函数的原型(prototype)

         3. 逐行执行代码

         4. 隐式返回这个对象

扩展知识:

        var o ={}=>var o = new Object()的语法糖    因为o的构造函数就是Object函数,在JS底层其实已经帮我们自动new(实例化)了一遍

        数组以及自定义函数也是一样的

        注意:

  •         所有原始数据类型(Number、String....)以及引用数据类型最终都是由Function衍生而来的,而Function是由JS引擎自带产生的
  •         而所有的构造函数原型的隐式原型都来源于Object构造函数原型,所以这就是我们常说的万物皆对象的原因

        

1.原型prototype

        (1)原型的定义:每个函数都有一个属性——prototype,在默认情况下,prototype的值就是一个普通的object对象(属性的集合),它有一个默认叫做constructor的属性,而constructor是用来指向这个函数本身(可以理解为函数的来源)。

        (2)原型的使用:当一个函数被用作构造函数来创建实例时,这个函数的prototype属性值会被作为原型赋值给所有对象实例(也就是设置 实例的__proto__属性),也就是说,所有实例的原型引用的是函数的prototype属性

        注意:

                1.prototype是函数(构造函数)的属性

                2.__proto__是对象的属性

Person.prototype.name = "man";//在Person构造函数的原型上创建一个name属性并赋值
Person.prototype.say = function(){//在Person构造函数的原型上创建一个say方法
	console.log("hello");
}
function Person(){}//创建一个构造函数
var person = new Person();//创建一个实例对象=>设置实例对象的__proto__属性(实例对象的原型链也就产生了)
console.log(person.name);// man
person.say();// hello

        (3)consturctor 构造函数

                定义:此属性只有原型对象才有,它默认指回prototype属性所在的构造函数

2.隐式原型 __proto__

       定义:所有对象上都有的一个属性,叫做隐式原型,__proto__,它指向创建该对象的构造函数的原型(prototype) ,即fn._proto_ === Fn.prototype      

                                                                                        用法: 

  •       当访问一个对象的成员是,如果该对象中存在这个属性,直接使用
  •       如果该属性不存在对象中,沿着该对象的隐式原型去查询
var obj = {
    a:1,
    b:2
}
obj.__proto__.c = 123;//往obj隐式原型直接添加c属性并赋值
console.log(obj.c)//123

注意:

       1. 每个构造函数原型的隐式原型都指向Object.prototype,但是Object.prototype一个特例——它的__proto__指向的是null,切记切记!!!(null其实就是JS给的一个出口值)

       2.Function.__proto__指向自身Function.prototype,因为上文提到所有构造函数都是由大的Function创建的,既然Function是函数,当然指向的是其本身了

下面我将上文总结一张图,便于知识的整体梳理:(图片来源于掘金网技术社区)

f:代表自定义函数        

 3.原型链     

             定义:原型链即是隐式原型 。

              为什么说原型链就是隐式原型,相信看完上图结构你已经有所明白了!

              因为每个对象和原型都有隐式原型,对象的隐式原型指向创建该对象的构造函数的原型对象,而父的原型又指向父的父,这种原型层层连接起来的就构成了原型链。

其实原型链的介绍就是隐式原型的介绍,下面我将附上几张图供大家作为原型链总结的参照

 1.函数是通过Function创建的

2.每个函数都有原型对象

 3.普通对象都是通过new 函数创建的

4.隐式原型的指向 

5. 原型中的constructor指向函数本身

 6.一条原型链的全貌

扩展知识:

  1. JavaScript中非常重要的两条链

  2. 原型链的查找属性是从下往上的过程,作用域链的查找是从上往下的过程

  3. 作用域链中找不到出现报错,原型链找不到不会报错,出现undefined

4.原型的应用

        1.基础方法:w3c不推荐直接使用系统属性                

1.Object.getPrototypeOf(对象) 获取对象的隐式原型                

        注意:

               ○  对象.__proto__   

               ○  [[scopes]] 、 __proto__ 系统属性不能被直接调用

2.Object.prototype.isPrototypeOf(指定对象) 判断当前对象是否在指定的对象的原型链上

3.对象 instanceof 函数:用来判断函数的原型在不在对象的原型链上 

4.Object.Create(对象) :创建一个空对象,并且将对象的隐式原型修改为指定的参数

        注意:在创建一个对象时,可以修改隐式原型,但是不要修改为null

5.Object.prototype.hasOwnProperty(属性名):判断某个对象属性名是不是对象自身的

       2.将伪数组转换为真数组

                ○  Array.isArray(伪数组)
                ○  Array.prototype.slice.call(伪数组)

        3.判断一个数组是不是真数组的常用方法

1.用 getPrototypeOf 方法获取对象的隐式原型 

var obj={};
var arr=[];
console.log(Object.getPrototypeOf(obj)===Array.prototype)//false
console.log(Object.getPrototypeOf(arr)===Array.prototype)//true

2.用 constructor 方法通过对象的构造函数判断

var obj = {};
var arr = [];
console.log(obj.constructor === Array)//false
console.log(arr.constructor === Array)//true

3.用 instanceof 操作符方法

 var obj = {};
 var arr = [];
console.log(obj instanceof Array); // false
console.log(arr instanceof Array); // true

4.用 Object.prototype.toString 方法来检测对象数据类型

var str = 123456;
//注意:原始类型是没有属性和方法的
//所有的数据在调用toString()时,调用的都是自身构造函数原型上的toString()
str.toString(); //'123456'

//所以必须去调用Object.prototype.toString()
console.log(Object.prototype.toString.call(123));//"[object Number]"

console.log(Object.prototype.toString.call(function(){}));//"[object Function]"

console.log(Object.prototype.toString.call(new Date()));//"[object Date]"

console.log(Object.prototype.toString.call([1,2,3]) === '[object Array]');//true

4.继承

        定义:不严格的说继承的本质就是复制,即重新创建一个原型对象,而新类型的实例继承了父原型里的所有属性

在学习继承之前,其实还有一个原型链的问题没讲,而我们即将要学的继承就是去弥补原型链产生的问题的

原型链的问题       

问题一: 当原型链中包含引用类型值的原型时,该引用类型值会被所有实例对象所共享;

问题二: 在创建子类型(例如创建Son的实例)时,不能向超类型(例如Father)的构造函数中传递参数.

总结:所以在我们真正的实践中会很少单独使用原型链,都是运用以下几种继承方法来弥补原型链产生的不足之处。

1.原型链继承 

function A(){
  this.nums= ["1", "2", "3"];
}
function B(){}

//关键点在于此行代码,创建一个A的实例对象,并将该实例对象赋值给B.prototype
//也就是被创建的A对象将继承B原型上所有的属性
B.prototype = new A();
var instance1 = new B();
instance1.nums.push("4");
console.log((instance1.nums)); //"1,2,3,4"

var instance2 = new B(); 
console.log((instance2.nums)); //"1,2,3,4"

原型链继承存在的问题:      

问题一:当子类实现继承后,会继承超类型原型上所有的属性,也就是说超类型原型的引用类型属性会被所有的子类访问到,这样继承原型引用类型属性的子类之间就不再具有自己的私有属性了。

问题二:在创建子类型的实例时,没有办法在不影响所有对象实例的情况下给超类型的构造函数中传递参数。
 

2. 借用构造函数继承

        为解决原型链中上述两个问题, 我们开始使用一种叫做借用构造函数。它是用来增强子类实例对象,等同于复制一遍超类(父类)的实例给子类。

function Father(){
    this.colors = ["red","blue","green"];
}
function Son(){
    Father.call(this);//继承了Father,且向超类型传递参数
    //扩展知识:如果有函数形参时可以用apply方法
    //Father.apply(this,arguments); arguments作为一个伪数组存储函数的每一个实参
}
var instance1 = new Son();
instance1.colors.push("black");
console.log(instance1.colors);//"red,blue,green,black"

var instance2 = new Son();
console.log(instance2.colors);//"red,blue,green" 可见引用类型值是独立的

核心代码是Father.call(this),创建子类实例时调用Father构造函数,于是Son的每个实例都会将Father中的属性复制一份。

缺点:      

  • 只能继承父类的实例属性和方法,不能继承原型属性/方法因为超类型的原型中定义的方法对于子类型是独立不可访问的
  • 因此每个方法都必须在构造函数中定义,所以无法实现函数复用,极度影响性能。

 3.组合继承

        也叫伪经典继承,将原型链和借用构造函数的技术组合到一块。用原型链实现对原型属性和方法的继承,用借用构造函数技术来实现实例属性的继承。  

function Father(name){
    this.name=name;
    this.colors=['red','yellow','pink'];
}
Father.prototype.sayName=function(){
    alert(this.name);
};
function Son(name,age){
    Father.apply(this.arguments);//继承Father原型的实例属性,第一次调用Father()
    this.age=age;
};
Son.prototype=new Father();//继承父类原型的实例方法,第二次调用Father()
Son.prototype.sayAge = function(){
	alert(this.age);
};
var instance1 = new Son("xm",3);
instance1.colors.push("black");
console.log(instance1.colors);//"red,yellow,pink,black"
instance1.sayName();//xm
instance1.sayAge();//3

var instance1 = new Son("xh",10);
console.log(instance1.colors);//"red,yellow,pink"
instance1.sayName();//xh
instance1.sayAge();//10

    组合继承避免了原型链继承和借用构造函数继承的缺点,且融合了它们的优点,也是JS中最常用的继承模式。而组合继承创建的对象也能够被instanceof和isPrototype()检测

虽然组合继承是JS中最常用的继承方式,但是它也存在了一个缺点,就是在使用子类创建实例对象时无论什么情况下都会调用两次超类型的构造函数,并且创建的每个实例中都要屏蔽超类型对象的所有实例属性,造成了不必要的内存消耗。

下面即将要讲的圣杯式继承就有效的解决了此问题,成为了JS最理想的继承模式

 4.传统继承——原型继承

        在object()函数内部, 先创建一个临时性的构造函数, 然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。(该方法这是由道格拉斯·克罗克福德所提出来的)

利用一个空对象作为中介,将某个对象直接赋值给空对象构造函数的原型。

function object(o){
	function F(){}
	F.prototype = o;
	return new F();
}

  而此方法返回值是一个引用传入对象的新实例,所以也就存在了某些数据被共享的问题

var Person = {
	colors : ["red","pink","blue"]
};
var PersonScd = Object(Person);//创建一个PersonScd实例对象,将Person原型上的所有属性赋值给它
PersonScd.colors.push("black");//往PersonScd的colors属性上添加一个值
var PersonTrd = Object(Person);
alert(Person.colors );//"red,pink,blue,black"

缺点:

  • 原型链继承多个实例的引用类型属性会被各个实例之间共享,且存在修改的可能。
  • 无法传递参数

另外,ES5中新增了Object.create()的方法,它能够有效代替传统继承方法。

5.寄生式继承 

        其实就是在原型式继承得到对象的基础上,在内部再以某种方式来增强对象,然后返回构造函数。

function createPerson(original){
  var clone = Object(original); // 通过调用 object() 函数创建一个新对象
  clone.sayHi = function(){  // 以某种方式来增强对象
    alert("hi");
  };
  return clone; // 返回这个对象
}

函数的主要作用是为构造函数新增属性和方法,以增强函数

寄生式继承的缺点跟原型式继承一样

6. 寄生组合式继承(圣杯式继承)       

        结合借用构造函数传递参数和寄生模式实现继承,因此圣杯模式成为了JS最理想的继承式方法

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.apply(this, arguments);
  this.age = age;
}

// 将父类原型指向子类
//此行代码等同于组合继承写的 SubType.prototype = new SuperType()
inheritPrototype(SubType, SuperType);

// 新增子类原型属性
SubType.prototype.sayAge = function(){
  alert(this.age);
}

总结:圣杯式继承的核心在于只调用了一次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 的方法。

此方法摘自掘金技术社区——‘木易杨说’

关于继承还有一个ES6——extends关键字方法,该方法主要用于类声明或者类表达式中,以创建一个类,该类是另一个类的子类。其中constructor表示构造函数,一个类中只能有一个构造函数,有多个会报出SyntaxError错误,如果没有显式指定构造方法,则会添加默认的 constructor方法

具体内容介绍请转自ES6 入门教程

全文总结:不管是学习哪个知识点,我们在学它之前都要带着三个W去学每一个内容,且学完记得多敲些相关的例题,相信你一定能够学有所成!!!

猜你喜欢

转载自blog.csdn.net/qq_60619943/article/details/122517540