Fork me on GitHub

JS原型,原型链和继承的理解

在学习JavaScript的过程中,经常会遇到几个难点,比如原型,原型链以及闭包之类的,下面我们就来聊聊我所理解的这些难点内容。

普通对象和函数对象

JavaScript是一门面向对象的语言,既然面向对象,那就得有对象才行对吧,所谓万物皆对象,但是对象之间也是有区别的,对象分为函数对象和普通对象。
函数对象可以创建普通对象,但是普通对象不能创建函数对象,普通对象在js的语法中什么特权也没有。
举例说明一下函数对象和普通对象:

var o1 = {}; 
var o2 =new Object();
var o3 = new f1();

function f1(){}; ==(等价于) var f1 = new Function();
var f2 = function(){};
var f3 = new Function('str','console.log(str)');

console.log(typeof Object); //function 
console.log(typeof Function); //function  

console.log(typeof f1); //function 
console.log(typeof f2); //function 
console.log(typeof f3); //function  

console.log(typeof o1); //object 
console.log(typeof o2); //object 
console.log(typeof o3); //object

以上例子中,o1,o2,o3都是普通对象,f1,f2,f3都是函数对象
凡是通过new Function创建的对象都是函数对象,其他都是普通对象(通常通过Object创建),可以通过typeof来判断。

构造函数

function Person(name,age,sex){
this.name = name;
this.age = age;
this.sex = sex;
this.sayName =function(){ alert(this.name)};
}

var person1 = new Person("lyc",21,"男");
var person2 = new Person("lyl",20,"女");

person1,person2都是Person的实例,它们都有一个constructor(指针)属性指向Person。

console.log(person1.constructor == Person);//true
console.log(person2.constructor == Person);//true

person1和person2是构造函数Person的实例化
实例的属性constructor永远指向它的构造函数


原型对象(终于进入主题了(-_-))

Javascript中,我们每定义一个对象(函数也是对象)时,JavaScript都会在对象中预定义一些属性。其中每个函数对象都有一个prototype属性,这个属性指向的是函数对象的原型对象。举个栗子:

function Person(){}  //创建一个函数对象
//利用函数对象的prototype属性在原型上设置属性                                                           
Person.prototype.name = "Nicholas";              
Person.prototype.age = 23;
Person.prototype.job = "Worker";
Person.prototype.sayName = function(){
        alert(this.name);
}
//创建实例化对象
var person1 = new Person();
var person2 = new Person();

console.log(person1.name);//Nicholas
console.log(person2.name);//Nicholas
console.log(person1.age);//23
console.log(person2.age);//23

每个实例对象都有一个proto属性,但只有函数对象才有prototype属性。
所谓原型对象:

Person.prototype = {
    name:"Nicholas",
    age: 23,
    job:"Worker",
    sayName:function(){alert(this.name)}
};

上面我设置了四个属性,其实它还有一个默认属性constructor属性,这个属性不指向Person这个函数对象了。
原型对象Person.prototype就是Person的实例

person1.__proto__ === Person.prototype;
Person.__proto__ === Function.prototype;//构造函数
Person.prototype.__proto__ === Object.prototype;
Object.__proto__ === Function.prototype;//构造函数
Object.prototype.__proto__ === null; 

所谓原型链其实就是实例对象的(proto)属性的隐式原型链;

属性的继承

原型属性的出现就是为了继承的,就像上面的栗子中,person1和person2的属性都是从它们共同的原型上继承而来的。当然person1和person2也可以有自己的属性。就好比,你可以继承家里的财产,同时你也可以有自己的的财产。
在JavaScript中有三继承方式分别是原型继承(也叫拷贝继承),构造函数实现继承(这是一种假的继承方式)和组合继承。

原型继承

原型继承其实就是借助已有的对象去创建子对象,把子对象的原型属性prototype指向父对象。

function Parents(name,age){
            this.name = name;
            this.age = age;

        }
        Parent.prototype.sayHi = function(name,age){
                alert("my name is "+this.name+"my age is "+this.age);
            };
        function Child(grade){
            this.grade = grade ;
        }
        Child.prototype.sayGrade = function(grade){
                alert("my grade is"+this.grade);
                };
        //把子类型的原型对象指向父类型的实例对象(很重要)
        Child.prototype = new Parents("林云","22");
        //把Child的原型对象的构造函数指向Child
        Child.prototype.constructor = Child;
        var chi= new Child(69);
        chi.sayHi();
        chi.sayGrade();

chi.prototype.__proto__ => Child.prototype,  Child.prototype.__proto__=>Parents.prototype

chi实例的原型是Child的实例,而Child的原型对象指向了Parents的实例;这样就实现原型继承 而chi拥有了Child和Parents的所有属性和方法;原型继承有优点也有缺点。

//父类:人
function Person () {
this.head = '脑袋瓜子';
this.emotion = ['喜', '怒', '哀', '乐']; //人都有喜怒哀乐
}
//子类:学生,继承了“人”这个类
function Student(studentID) {
    this.studentID = studentID;
    }
Student.prototype = new Person();
Student.prototype.constructor = Student;

var stu1 = new Student(1001);
console.log(stu1.head); //脑袋瓜子
console.log(stu1.emotion); //['喜', '怒', '哀', '乐']

stu1.head ="愚蠢的脑袋瓜子";
console.log(stu1.head)//愚蠢的脑袋瓜子
stu1.emotion.push('愁');
console.log(stu1.emotion); //["喜", "怒", "哀", "乐", "愁"]

var stu2 = new Student(1002);
 console.log(stu2.head); //脑袋瓜子  原型上的head没有stu1所修改
console.log(stu2.emotion); //["喜", "怒", "哀", "乐", "愁"]   而emotion就被stu1所修改了

从上面的例子可以看出:原型上任何类型的属性值都不会通过实例被重写,但是引用类型( 数组,对象,函数)的属性值会受到实例的影响而修改。

构造函数实现继承(call,apply方法)

function Parents(name,age){
            this.name = name;
            this.age = age;
    }

function Child(name,age,grade){ 
    this.grade = grade ;
    //Person.call(this,name,age);   //this指向Child的实例对象  this.Parent(name.age);
    //Person.apply(this,[name,age]);
    Person.apply(this,arguments);
}

var chi= new Child("林云",22,69);
console.log(chi.name);
console.log(chi.age);

缺点:子类对象会拷贝父类对象的每一个方法,而实例也会拷贝,这样一来内存空间造成浪费。而且当需求发生改变时,要改动其中的一个方法时,之前所有的实例,他们的该方法都不能及时作出更新。只有后面的实例才能访问到新方法。

组合继承

组合继承是利用原型继承和构造函数继承两者的优点来实现属性和方法的继承

function Parent(name,age){
    this.name = name;
    this.age = age;
} 
Parent.prototype.Say =function(){
    console.log("这是一个父类型的原型对象");
}
function Child(name,age,grade){
    this.grade = grade;
    Parent.call(this,name,age);
    //Parent.apply(this,[name.age]);
    //parent.apply(this,arguments);
}
Child.prototype = new Parent();
Child.prototype.sayGrade = function(grade){
    console.log("This is my grade:"+grade);
}
Child.prototype.constructor = Child;
var chi = new Child("林云",22,100);
chi.Say();
chi.sayGrade();

而我们利用原型继承去继承原型上的方法,利用构造函数继承来继承原型上的属性。从而避免原型上的属性被修改以及方法被实例拷贝。
注意:属性方在本身对象内,方法放在原型对象内

-------------本文结束感谢您的阅读-------------
坚持原创技术分享,您的支持将鼓励我继续创作!