原型和原型链 | 青训营笔记

102 阅读4分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 6 天

原型和原型链

什么是原型和原型链

在 JavaScript 中,每个对象都有一个原型对象,原型对象也是一个普通的对象,它有一个 constructor 属性指向创建它的构造函数,它也有一个原型对象,这样就构成了一个原型链。在 JS 中,面向对象的实现是基于原型的,而不是基于类的。就算 ES6 中引入了 class 关键字,但是它只是语法糖,本质上还是基于原型的。

class 和继承

class Student{
    // 构造函数
    constructor(name, number){
        this.name = name;
        this.number = number;
        // ...
    }
    // 方法
    sayHi(){
        console.log(`姓名:${this.name},学号:${this.number}`);
    }
    // ...
}

// 通过类声明对象(实例)
const Zhangsan = new Student('张三', 100);
console.log(Zhangsan.name); 
Zhangsan.sayHi();

这里使用了模板字符串来拼接字符串,模板字符串是 ES6 中新增的语法,它可以很方便的拼接字符串,它的语法是使用反引号来包裹字符串,然后在字符串中使用${} 来引用变量。

同很多面向对象的语言一样,我们可以使用 extends 关键字来实现继承,然后使用 super 来继承属性。

class Person{
    constructor(name){
        this.name = name;
    }
    eat(){
        console.log(`${this.name} eat something`);
    }
}

class Student extends Person {
    constructor(name,number){
        super(name);
        this.number = number;
    }
    sayHi(){
        console.log(`姓名:${this.name},学号:${this.number}`);
    }
}

const Zhangsan = new Student('张三', 100);
console.log(Zhangsan.name); // 张三
Zhangsan.sayHi();   // 姓名:张三,学号:100
Zhangsan.eat();    // 张三 eat something

class Teacher extends Person {
    constructor(name,major){
        super(name);
        this.major = major;
    }
    teach(){
        console.log(`${this.name} 教授 ${this.major}`);
    }
}

const Wanglaoshi = new Teacher('王老师', '语文');
console.log(Wanglaoshi.name);   // 王老师
Wanglaoshi.teach(); // 王老师 教授 语文
Wanglaoshi.eat();   // 王老师 eat something

原型

类型判断 instanceof

instanceof 是用来判断一个对象是否是某个类的实例,它的语法是 object instanceof constructor,其中 object 是要判断的对象,constructor 是构造函数。

Wanglaoshi instanceof Teacher;  // true
Wanglaoshi instanceof Person;   // true
Wanglaoshi instanceof Student;  // false
Wanglaoshi instanceof Object;   // true
[] instanceof Array; // true
[] instanceof Object;   // true
{} instanceof Object;   // true

原型

typeof Student // function
typeof Person // function

因此我们可以看出,这里的类实际上是使用 function 来实现的,这里的类语法只是语法糖,本质上还是基于原型的。

Student.prototype // {constructor: ƒ, sayHi: ƒ}
Person.prototype // {constructor: ƒ, eat: ƒ}

原型分为隐式原型和显示原型,隐式原型是对象的 __proto__ 属性,显示原型是函数的 prototype 属性。

console.log(Zhangsan.__proto__ === Student.prototype); // true
console.log(Zhangsan.__proto__); // {constructor: ƒ, sayHi: ƒ}
console.log(Student.prototype); // {constructor: ƒ, sayHi: ƒ}
  • Student
属性
prototype指向 Student.prototype
  • Zhangsan
属性
name张三
number100
__proto__指向 Student.prototype
  • Student.prototype
属性
sayHiƒ sayHi()

性质

  • 每个 class 都由显式原型 prototype
  • 每个实例都有隐式原型 __proto__
  • 实例的隐式原型指向类的显式原型

执行规则

获取属性 Zhangsan.name 或执行方法 Zhangsan.sayHi()时:

  • 先在自身属性和方法中查找,找到的是自己的属性和方法,就直接使用
  • 如果没有找到,就会沿着 __proto__ 这条链向上查找,直到找到为止,这就实现了继承

原型链

console.log(Person.prototype === Student.prototype.__proto__); // true

这个表达式表明,Student.prototype 的隐式原型指向 Person.prototype

Zhangsan.hasOwnProperty('name'); // true
Zhangsan.hasOwnProperty('sayHi'); // false

hasOwnProperty 方法是用来判断一个属性是不是自身的属性,而不是继承的属性。

最终的原型链是这样的:

proto

console.log(Object.hasOwnProperty('hasOwnProperty')); // true

instanceof 的原理 ⭐

const zs = new Student('张三',1234);

// 自己实现 intanceof
/**
 * @ param L 待判断的
 * @ param R 被匹配的
 */
function instance_of(L,R){
  let O = R.prototype;
  L = L.__proto__;

  while(true){
    // 查找到原型链顶端的 Object (object.__proto__ === null)
    if(L === null){
      return false;
    }
    if (O === L){
      return true;
    }
    L = L.__proto__;
  }
}

console.log(instance_of(zs,Person))

这是一个逐级向上查找的过程,直到找到 R.prototype 为止。如果最终没有找到,就返回 falseL===null 表示已经到了原型链的顶端的 object,还没有找到,所以返回 false

重要提示

  • class 是 ES6 的语法规范,由 ECMA 委员会发布
  • ECMA 只指定了语法规范,不指定具体的实现
  • 以上的原型链是 V8 引擎的实现,其他引擎的实现可能不同

例子

  • 如何判断一个对象是不是数组?

答:使用 instanceof 来进行判断。

  • 手写简易的 jQuery ,考虑插件和扩展性。

jQuery 最核心的功能就是选择 DOM 元素,然后对其进行操作。

<div id="app">hello world</div>
<p>你好</p>
<p>嘿嘿</p>
class jQuery {
  constructor(selector) {
    const result = document.querySelectorAll(selector)  // 查询 dom 元素
    const length = result.length
    for (let i = 0; i < length; i++) {
      this[i] = result[i]
    }
    this.length = length
    this.selector = selector
  }
  get(index) {
    return this[index]  // 返回 dom 元素
  }
  each(fn) {
    for (let i = 0; i < this.length; i++) {
      const elem = this[i]
      fn(elem)  // 执行回调函数
    }
  }
  on(type, fn) {
    return this.each(elem => {
      elem.addEventListener(type, fn, false)    // 绑定事件
    })
  }
  // 扩展
}

const $p = new jQuery('p')

console.log($p) // jQuery {0: p, 1: p, length: 2, selector: "p"}

console.log($p.get(1))  // <p>嘿嘿</p>

$p.on('click',()=>{
  alert('clicked')  // 点击 p 标签,弹出 clicked
})

我们可以通过原型链直接扩展 jQuery 的功能,如下:

// 插件,直接向原型中添加
jQuery.prototype.dialog = function (info){
  alert(info)
}

$p.dialog('asd')    // 弹出 asd

同时可以通过继承 jQuery 来扩展功能,如下:

class myJQuery extends jQuery {
  constructor(selector) {
    super(selector)
  }
  // 扩展
}
  • class 的原型本质,如何理解?

如上面所述。