阅读 100

js回顾:原型链

原型链

原型链实际上是JavaScript中的实现继承的机制,在搞懂原型链之前首先要搞懂几个概念:this,普通对象和函数对象,构造函数,new

this

this对于很多人来说是混杂不清的概念,但是想要弄清楚原型链我们必须了解什么是this

首先,this只能在存在与函数中

其次,this其实是当前函数所在上下文环境,再简单一点也可以理解为this返回一个当前函数所在的对象,也就是说想要知道this是什么我们只需要关注是谁调用了this所在的函数就可以了

如下边的代码zhao.sayName()是zhao调用的sayName函数所以sayName中的this自然指的就是zhao这个对象,而下方 var liName = zhao.sayName语句是将sayName这个函数赋值给liName,调用liName就相当于在最顶层也就是window下直接调用sayName,this的指向自然就是window这个最顶层对象

    var name = "Li"
    var zhao = {
        name: "Zhao",
        sayName: function () {
            console.log(this.name);
        }
    }
    zhao.sayName() // Zhao
    var liName = zhao.sayName;
    liName() // Li
复制代码

普通对象与函数对象

JavaScript中一切都可以看作对象,但是实际上对象也是有区别的,对象分为普通对象函数对象

    // 普通对象
    var o1 = {}
    var o2 = new Object()
    var o3 = new f1()
    // 函数对象
    function f1(){}
    var f2 = function(){}
    var f3 = new Function()

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

    console.log(typeof o1); //object
    console.log(o1.prototype); //undefined
    console.log(typeof o2); //object
    console.log(o2.prototype); //undefined
    console.log(typeof o3); //object
    console.log(o3.prototype); //undefined
复制代码

凡是通过function构建的对象都是函数对象,并且只有函数对象才有prototype属性,普通对象没有

prototype 原型

prototype又是什么呢?

当我们创建函数的时候,编译器会自动为该函数创建一个prototype属性,这和属性指向一个包含constructor属性的对象,而这个属性又默认指回原函数,读起来有点绕对吧,大概是这样的

    function Person() {
    //    prototype = {
    //        constructor: Person,
    //    }
    }
复制代码

每个函数对象都有一个prototype(原型)属性,在我看来prototype属性的意义:

  1. 创建对象的模板
  2. 公开的共享空间

这两点等学习了下边new命令你就会明白了

constructor 构造函数

函数对象的一种用法就是构造函数,通过构造函数可以构建一个函数对象的实例(普通对象)

    function Person(name, age ){
        this.name = name;
        this.age = age;
        this.sayHello = function(){
            console.log(`Hello! my name is ${this.name}`);
        };
    }

    var person1 = new Person("kidder", 28);
    person1.sayHello(); // Hello! my name is kidder
    console.log(person1.constructor); //[Function:Person]
复制代码

按照惯例,构造函数的命名以大写字母开头,非构造函数以小写字母开头,通过构造函数构造的普通对象都会有一个constructor(构造函数)属性,该属性指向构造该对象的构造函数

new命令

new命令的工作机制

  1. 创建一个空对象作为要返回对象的实例

  2. 将这个空对象的原型(__ proto __)指向构造函数的prototype属性

  3. 将这个空对象赋值给构造函数内部的this

  4. 执行构造函数内部的代码

原型链

下面我们来看看构造函数构建一个普通对象的时候发生了什么

    var Person = function (name) {
        this.name = name;
        this.age = 18;
    };
    Person.prototype.sayHello = function(){
        console.log(`Hello! my name is ${this.name}`);
    };
    var li = new Person("Li");
    console.log(li.name); // Li
    console.log(li.age); // 18
    li.sayHello(); // Hello! my name is Li
复制代码
  1. 创建一个空对象作为要返回对象的实例
    {}
复制代码
  1. 将这个空对象的原型(__ proto __)指向构造函数的prototype属性
    {
        __proto__:Person.prototype;
    }
复制代码
  1. 将这个空对象赋值给构造函数内部的this
    this = {
        __proto__:Person.prototype;
    }
复制代码
  1. 执行构造函数内部的代码
   this = {
        __proto__:Person.prototype;
       name: "Li";
       age: 18;
   }
复制代码

所以li这个对象中只有name和age两个属性,为什么li.sayHello()会输出Hello! my name is Li呢?

这就是原型链,当给定的属性在当前对象中找不到的情况下,会沿着__proto__这个属性一直向对象的上游去寻找,直到__proto__这个属性指向null为止,如果找到指定属性,查找就会被截断,停止

上面这张图是整个JavaScript的原型链体系,为了让这张图更直观所以我将构造函数的prototype属性单独提了出来,恩,其实画在构造函数内部也可,但同时因为对象是引用类型,所以这样画也没毛病吧

__ proto __ 和 prototype

这两个属性经常会被我们混淆,那么我们回过头再来总结一下

prototype:只有函数对象才具有的属性,它用来存放的是构造函数希望构建的实例具有的共享的属性和方法,主要用于构造函数的实例化

__ proto __ : 所有对象都具有的属性,它指向的是当前对象在原型链上的上级对象,主要作用是让编译器在由__proto__这个属性构成的原型链上查找特定的属性和方法

补充

prototype的共享属性

    var Person = function (name) {
        this.name = name;
        this.age = 18;
    };
    Person.prototype.sayHello = function(){
        console.log(`Hello! my name is ${this.name}`);
    };
    var li = new Person("Li");

    var Person1 = function () {
    };
    Person.prototype.name = "Li"
    Person.prototype.age = 18
    Person.prototype.sayHello = function(){
        console.log(`Hello! my name is ${this.name}`);
    };

    var Li = new Person1();

复制代码

关于Person和Person1两种构造函数的写法有什么不同呢?

一般来说写在prototype原型对象中的属性和方法都是公用的,也就是说写在构造函数中的属性在构建普通对象的时候,都会在新对象中重新定义,也就是从内存的角度来说又会多占用一些内存空间,所以我们将构造函数的所有属性和方法都写在prototype原型中不好吗?

但是原型函数也是有缺点的:

  1. 不够灵活
    var Person = function () {
    };
    Person.prototype.name = "Li"
    Person.prototype.age = 18
    Person.prototype.sayHello = function(){
        console.log(`Hello! my name is ${this.name}`);
    };

    var li = new Person();
    var zhao = new Person();
复制代码

这种方式构造的所有对象都是一个模板,虽然我们也可以在当前对象下进行修改,但这样一点也不优雅,不规整,而且从某种意义上来说也是对内存的浪费

  1. 对于引用类型的修改会被全部共享
    var Person = function () {
    };
    Person.prototype.name = "Li"
    Person.prototype.age = 18
    Person.prototype.friends = ["ZhangSan", "LiSi"]
    Person.prototype.sayHello = function(){
        console.log(`Hello! my name is ${this.name}`);
    };

    var li = new Person();
    var zhao = new Person();
     li.friends.push("WangWu");
    console.log(zhao.friends); // [ 'ZhangSan', 'LiSi', 'WangWu' ]
复制代码

在JavaScript中,基本类型的修改可以明确的通过创建或修改在当前对象下的属性对原型链进行截断,但是像数组,对象这种引用类型的值虽然也可以通过在当前对象中创建该属性来对原型链进行截断,但是一不注意就可能会出现上面这种情况直接对原型进行了修改

构造函数与原型相结合

所以,用构造函数来定义实例属性,用原型定义方法和共享的属性,这样写就比较优雅了

    function Person(name, age){
        this.name = name;
        this.age = age;
        this.friends = ["ZhangSan", "LiSi"];
    }
    Person.prototype.sayHello = function(){
        console.log(`Hello! my name is ${this.name},${this.age}岁了`);
    };
    var li = new Person("li", 18);
    var zhao = new Person("zhao", 16);
    li.sayHello();
    // Hello! my name is li, 18岁了
    zhao.sayHello();
    // Hello! my name is zhao,16岁了
    li.friends.push("WangWu");
    console.log(zhao.friends);
    // [ 'ZhangSan', 'LiSi' ]
复制代码

创建对象的几种方式

  1. 构造函数方式

    法一用构造函数构造一个新对象

    var A = function () { };
    var a = new A();
    console.log(a.constructor); // [Function:A]
    console.log(a.__proto__ === A.prototype); //true
复制代码

  1. 字面量方式

    法二的本质来说和法一是一样的,就是隐式调用原生构造函数Object来构造新对象

    var a = {};
    // var a = new Object();
    console.log(a.constructor); // [Function:Object]
    console.log(a.__proto__ === Object.prototype); //true
复制代码

  1. create方式

    法三Object.create是以一个普通对象为模板创建一个新对象

    var a1 = {a:1}
    var a2 = Object.create(a1);
    console.log(a2.constructor); // [Function:Object]
    console.log(a2.__proto__ === a1);// true
    console.log(a2.__proto__ === a1.prototype); //false
复制代码

所以除了Object.create创建对象的方式,可以说:__ proto __ === constructor.prototype;

constructor

前面我们说道prototype的时候进行原型属性的赋值的时候,采用的是逐项赋值,那么当我直接将对象赋值给prototype属性的时候会发生什么呢?

    function Person() { }
    Person.prototype = {
        name : "Li",
        age : 18,
        sayHello : function () {
            console.log(`Hello! my name is ${this.name},${this.age}岁了`);
        }
    };
    var li = new Person();
    console.log(li instanceof Object);        // true
    console.log(li instanceof Person);        // true
    console.log(li.constructor === Person);    // false
    console.log(li.constructor === Object);    // true
    console.log(Person.prototype.constructor);  // Object
复制代码

这时候我们就发现我们构建的li对象的constructor不再指向它的构造函数Person,而是指向了Object,并且Person原型Person.prototype的constructor指向也指向了Object,这是什么原因呢?

其实,根源出现在Person.prototype上,上边我们提到过,其实我们在写构造函数的时候实际上是这样的

    function Person() {
    // prototype = {
    //    constructor : Person
    // }
    }
复制代码

当我们构建Person构造函数的时候,编译器会自动生成一个带有指向Person的constructor属性的对象,并把这个对象赋值给Person.prototype,我们又知道js中对象是引用类型,当我们使用Person.prototype.name=...的时候实际上是对这个对象的修改,而使用Person.prototype={...}实际上是将这个属性原本的指针指向了另一个新创建的对象而不是原来编译器自动创建的那个:

而li的constructor属性自然是继承自Person.prototype,所以constructor自然也就跟着改变了,如果在编程的过程中constructor这个属性很重要的话可以通过下面的方式

    function Person() { }
    Person.prototype = {
        constructor:Person
        name : "Li",
        age : 18,
        sayHello : function () {
            console.log(`Hello! my name is ${this.name},${this.age}岁了`);
        }
    };
    
    var li = new Person();
    console.log(li instanceof Object);        // true
    console.log(li instanceof Person);        // true
    console.log(li.constructor === Person);    // true
    console.log(li.constructor === Object);    // false
    console.log(Person.prototype.constructor);  // Person
复制代码

结语:

参考:《JavaScript高级程序设计》

这是我对JS原型链部分的总结与思考,也是我写的第一篇这么正式的技术文档,如有纰漏之处,欢迎大家批评指正

关注下面的标签,发现更多相似文章
评论