【JS基础】对原型和原型链的追溯

228 阅读12分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 2 天,点击查看活动详情

目标

  • 理解对象
  • 理解对象创建变化过程
  • 理解原型
  • 理解原型链

顺着下文加粗的问题X就可以理解为什么这样改进

ES6之前如何实现类

是用于创建对象的模板。他们用代码封装数据以处理该数据。JS 中的类建立在原型上——MDN

在ES6之后,JavaScript 才正式支持了类和继承的语法,ES6之前是没有类的写法,而是通过原型实现的类。

实际上,类是“特殊的函数”,就像你能够定义的函数表达式函数声明一样,类语法有两个组成部分:类表达式类声明。看懂了下面的过程,就知道ES6之前如何实现类的了

理解对象

对象的字面定义:一组属性的无序集合

可以把对象想成一张散列表,其中的内容就是key:value 其中value可以是数据也可以函数,如下代码所示,name,age这些就是key

    let person = {
        name : "xiaoming";   
        age : 20;   
        job : "student";  
        sayName(){
            console.log(this.name);
        }
    }

理解对象创建变化过程

在ES6之前创建对象的三种方法:

  1. 利用对象字面量创建
    let person = {
        name : "xiaoming";   //person对象属性1
        age : 20;   //person对象属性2
        job : "student";   //person对象属性3
        sayName(){
            console.log(this.name);
        }
    }
  1. 利用new Object()创建
    let person = new Object();  //创建了一个名为person的对象
    person.name = "xiaoming";  //person对象属性1
    person.age = 20;  //person对象属性2
    person.job = "student";  //person对象属性3
    person.sayName = function()  //person对象方法
    {
        console.log(this.name);
    }

虽然使用对象字面量和new Object可以方便的创建对象,但是这两种方式也存在明显不足,如果要创建100个这样的对象呢?用for?即使可以用,但是会消耗大量的内存空间放重复的东西,比如这个地方的sayName(问题1

即使利用工厂模式解决多次创建多个对象的问题,但不能明确的是:新创建的对象是什么类型?(问题2

而ECMAScript的构造函数适用于创建特定类型的对象(如 Array等)那么我们也可以自定义,创建类似Person这样的对象

  1. 利用构造函数创建对象
function Person(name,age,job)
  {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName(){
       console.log(this.name);
      }
  }
var xiaoming = new Person(xiaoming,18,student);
var xiaohong = new Person(xiaohong,20,student);

构造函数和普通函数没有区别,唯一的区别就是调用的方式不同 看下面三个例子:

let obj = new Person(xiaoming,18,student); //用new作为构造函数调用
obj.sayName(); // 会打印出“xiaoming”

Person(xiaoming,18,student); //不加new作为普通函数调用
window.sayName(); // 不加指定的this会默认添加到window上去(浏览器中,node中是global)

console.log("下一步学习可以继续研究一下this的指向问题")

构造函数中的属性方法我们统称为成员,这些成员是可以添加的

实例成员就是构造函数内部通过this添加的成员(如name,age,job,sayName()就是实例成员)

实例成员只能通过实例化的对象来访问 ,不可以通过构造函数来访问 如:Person.name是访问不到的(这个地方我记得和数据属性的四个特性有关?)

静态成员就是在构造函数本身身上添加的成员 如:Person.sex = "男"

静态成员只能通过构造函数来访问 ,不可以通过对象来访问

理解原型

由此,构造函数看似已经解决了上述的问题1和问题2

但是构造函数也不是没有问题,有什么问题呢?

我们知道,在Person里声明定义sayName()方法的时候,它作为函数,而在ECMA中的函数是对象(数据类型为:引用数据类型),会在堆中重新开辟地址来存放,造成多少个对象就会浪费多少个相同函数的内存(问题3

如下代码所示,在调用sayName()的时候明明都是实现同样的逻辑却要占用三份相同的内存

var obj1 = new Person(xiaoming,18,student);
var obj2 = new Person(zhangsan,30,worker);
var obj3 = new Person(lisi,40,free);
obj1.sayName();
obj2.sayName();
obj3.sayName();

所以,引入了在构造函数中引入原型prototype,而这个prototype就是用来解决问题3的。(这种方法也叫原型模式创建对象)

我们向构造函数中添加一个prototype属性,而这个属性里面装的是什么呢?就是类似sayName()一类的公共方法

(但也不是说prototype上只能放方法,name这类属性也可以放,只要是共同不变的对象,避免重新开辟内存都可以放在上面)

prototype这个属性在构造函数Person中的形式是prototype:{ object } (可以自己打印出来看看)

(prototype翻译过来是原型,并且 { } 代表是一个对象,所以叫原型对象

由此,我们可以把那些不变的方法直接定义在原型对象上,这样所有的实例对象都会共享这些方法不需要再开辟内存空间

function Person(name,age,job)
  {
    this.name = name;
    this.age = age;
    this.job = job;
  }
Person.prototype.sayName = function(){
      console.log(this.name);
    }
var obj1 = new Person(xiaoming,18,student);
var obj2 = new Person(zhangsan,30,worker);
var obj3 = new Person(lisi,40,free);
obj1.sayName();
obj2.sayName();
obj3.sayName();

阶段总结1:js规定,只要是创建一个函数(刚已经说了构造函数和普通函数都是函数)里面就有一个prototype的属性,这个prototype里面装的是公共的属性和方法

再来,方法定义在构造函数身上,为什么实例化的对象能访问到?也就是说obj1是如何和Person.prototype.sayName()建立的连接?(问题4

因为实例化对象的身上有一个对象原型 __proto__或者[[prototype]]

阶段总结2:同样,js规定每个对象都有一个属性__proto__指向构造函数的prototype(比如{}.__proto__=== Object.prototype),之所以我们对象可以使用构造函数上的属性和方法都是因为__proto__的存在

obj1.__proto__<===> Person.prototype  (显示的是object) 两者等价true
(Person.prototype.__proto__ === Object.prototype)

但其实这样写还是有点问题,什么问题呢?如果我这个公共的方法很多(注释掉的部分),看起来也不太美观(问题5),我们就采取对象的方式打包起来直接赋值给Person的prototype

//自有属性写在构造函数里面
function Person(name,age,job)
  {
    this.name = name;
    this.age = age;
    this.job = job;
  }
//Person.prototype.constructor = Person 任何一个非箭头函数的prototype.constructor都等于它本身
//Person.prototype.sayName = function(){
      //console.log(this.name);
    //}
//Person.prototype.sayJob = function(){
      //console.log(this.job);
    //}    
//共有属性写在原型上面
Person.prototype = {
  //constructor:Person; 没有这一句的话
  sayName : function(){
    console.log(this.name);
  },
  sayJob : function(){
    console.log(this.job);
  },
}
var obj1 = new Person(xiaoming,18,student);
console.log(Person.prototype);//这个时候打印出来就没constructor属性了,取而代之的是一个对象
console.log(obj.__proto__);

但如果我们修改了原型对象的写法,给原型对象赋值的是一个对象而不是一个函数,在这种情况下,console.log(Person.prototype) 打印出来,取而代之的是一个对象

带来的问题就是没有原型,他俩到底是谁的孩子(之前提到的问题2),就不清楚了 所以我们需要手动添加constructor属性指回原来的构造函数Personconstructor:Person

阶段总结3:如果我们修改了原型对象的写法,给原型对象赋值的是一个对象而不是一个函数,则必须手动的利用constructor属性指回原来的构造函数Person

结合上面的三个阶段总结就可以看懂这张图了

d8d2f5ccd4879e66a3e81e4d7b6095a.jpg

这也保证了我们能顺着原型链找到成员

1677590158769.png

有没有感受到面向对象的封装继承多态的味道了,有了原型链的形式我们就可以继续提出继承,在此基础上继承要如何实现呢?ES6之后正式支持面向类和继承,它又是如何实现的呢?

ES6之后如何实现类

其实说简单一点就是语法糖,进行了封装,这是一个用ES6语法定义类的基本示例,可以看到没有了prototype

class Person {
  //自有属性写在constructor里面
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  //共有属性写在constructor外面
  sayHello() {
    console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
  }
}

const person = new Person('John', 25);
person.sayHello();

在上面的示例中,我们还是声明了一个名为 Person 的类。类的构造函数 constructor 用于初始化对象的属性,sayHello 是类的一个方法。通过 new 关键字可以实例化一个类,并调用类的方法。

注意:类声明函数声明之间的一个重要区别在于,函数声明会提升,类声明不会。在类中我们首先需要声明类,然后再访问它,否则代码将抛出ReferenceError

总结两种类的写法

使用原型的写法有一个缺点,在TS里面不好写,所以用TS的话尽量使用class的写法

方法一:使用原型 (ES5没有class如何实现类)
function Person(name,age)
  {
    this.name = name;
    this.age = age;
  }
//可共享的放到prototype上面
Person.prototype.sing = function(){
      console.log("我会唱歌");
    }
var p1 = new Person(ldh,18);
var p2 = new Person(fy,5);


方法二:使用classES6有了class之后)
// class 没有提供添加非函数属性的方法 prototype
class Person
{
  //本身的属性写到constructor上面
  constructor(name,age){
    this.name = name;
    this.age = age;
  }
  //共有的方法写到外面
  sing(){
      console.log("我会唱歌");
    }
}
var p3 = new Person(zzz,18);

js的new做了什么

这个地方new Person('John', 25)做了什么呢?

  1. 先创建了一个临时空对象 {}
  2. 给临时对象绑定原型Person
  3. 指定this = 临时对象
  4. 执行构造函数
  5. 返回临时对象,用const person接收到

类体static,private,protect,public有什么区别

在 JavaScript 中,类体内方法前面的 staticprivateprotectedpublic 是访问修饰符,用于定义类的成员的可访问性和作用域。

1、static

static:静态方法属于类本身,而不是类的实例。可以直接通过类名调用,无需实例化对象。静态方法不能访问实例的属性和方法,只能访问静态属性和调用其他静态方法。

class MyClass {
  static staticMethod() {
    console.log('This is a static method.');
  }
}
MyClass.staticMethod(); // 类调用静态方法,不能通过实例对象调用

2、private

private:私有方法只能在类的内部访问,外部无法访问。可以使用 # 符号声明私有方法。

class MyClass {
  #privateMethod() {
    console.log('This is a private method.');
  }
}

3、protected

protected:受保护方法只能在类的内部和子类中访问,外部无法访问。使用 protected 关键字定义受保护方法。

所以protected比private多的就是子类中可以访问

class MyBaseClass {
  protected protectedMethod() {
    console.log('This is a protected method.');
  }
}

class MySubClass extends MyBaseClass {
  someMethod() {
    this.protectedMethod(); // 可以访问父类的受保护方法
  }
}

4、public

public:公共方法是默认的访问修饰符,可以在类的内部和外部访问。

class MyClass {
  publicMethod() {
    console.log('This is a public method.');
  }
}

const myObj = new MyClass();
myObj.publicMethod(); // 调用公共方法

继承

ES6之前的继承——组合继承

ES6之前并没有提供继承extents 我们可以通过借用父构造函数+原型对象来模拟实现继承,被称为组合继承

继承属性用call()

子构造函数中用call()调用父构造函数以实现对父构造函数中属性的继承

1 实现函数的调用 可以等于 fn() = fn.call()

2 可以改变一个函数的指向 后面的传参就任意传,o只是用来改变指向

3 call的第一个参数this如果没有说明那就是全局对象window,在严格模式下是undefined

子构造函数继承父构造函数中的两个属性 核心就是,在子构造函数中调用父构造函数并使用call把父构造函数的this指向子构造函数的this

//父构造函数
function Product(name, price) {
  this.name = name;
  this.price = price;
} 
//子构造函数
function Food(name, price) {
  Product.call(this, name, price); //子构造函数中调用父构造函数,并使用call把父构造函数的this指向子构造函数的this
  this.category = 'food';
} 
function Toy(name, price) {
  Product.call(this, name, price);
  this.category = 'toy';
}
var cheese = new Food('feta', 5);
var fun = new Toy('robot', 40);

继承方法

继承属性使用call(),那如何继承方法呢?

我们可能首先想到,直接把放公共函数prototype赋值,比如son.prototype = father.prototype

但这样的做法是不对的,这样相当于引用,两个都指向了同一块儿堆区内存

在子上面添加的方法也会到父上面去

// son.prototype.__proto__ = father.prototype //这句话被ban了
son.prototype = new Father(); //new Father会产生一个father的实例对象temp
//new会做四件事情
1 创建临时对象
2 this = 临时对象
3 this.__proto__ = 构造函数的 prototype
4 执行构造函数
5 return this
//而这个temp 是 === Father.prototype的 原型链
console.log(Son.prototype.constructor) //指向了Father

但是如果利用对象的形式修改了原型对象,别忘了利用constructor 指回原来的构造函数

// son.prototype = father.prototype
son.prototype = new Father(); //new Father会产生一个father的实例对象temp{}
//而这个temp 是 === Father.prototype的 原型链
son.prototype.constructor = Son;

ES6之后的继承——extent

ES6之后继承是通过使用 extends 关键字来实现的。子类可以继承父类的属性和方法,并可以扩展或重写它们。 以下是使用继承创建子类的示例:

class Student extends Person {
  constructor(name, age, major) {
    super(name, age); // 调用父类的构造函数
    this.major = major;
  }

  introduce() {
    console.log(`I'm studying ${this.major}.`);
  }
}

const student = new Student('Alice', 20, 'Computer Science');
student.sayHello(); // 输出: Hello, my name is Alice and I'm 20 years old.
student.introduce(); // 输出: I'm studying Computer Science.

在上面的示例中,我们定义了一个名为 Student 的子类,它继承了 Person 父类。子类的构造函数中使用 super 关键字来调用父类的构造函数,并可以添加自己的属性和方法