Skip to content
On this page

原型

背景知识

在基于类的传统面向对象的编程语言中,对象由类实例化而来,实例化的过程中,类的属性和方法会拷贝到这个对象中;对象的继承实际上是类的继承,在定义子类继承于父类时,子类会将父类的属性和方法拷贝到自身当中。因此,这类语言中,对象创建和继承行为都是通过拷贝完成的。

但在JavaScript,它是没有的概念的!没有类!没有类!

对象的“继承”并是不存在拷贝行为的,而是一种对象间的原型引用关系(在小黄书中把其叫做行为委托)

ES6 中的class也只不过是语法🍬 ,本质还是建立在 [[prototype]] 机制上,并非真正意义上的类。

想必你们都被它骗了吧?坏得很!

先来认识一下对象和函数

在JavaScript中,对象分为普通对象和函数对象。

我们先分别来看看一个普通对象/函数里有什么,在浏览器下执行以下的代码:

javascript
let obj = {}
console.dir(obj)
console.log("obj 的类型"+typeof obj);
function Fn() {}
console.dir(Fn)
console.log('Fn 的类型' + typeof Fn);

运行结果如下:

2021-12-16-01-05-02-image

我们可以看到:

  • 对于一个普通对象来说,隐含着一个属性,我们可以称之为 原型链[[prototype]]的一个节点 。
    • 对象更深入的认识,我们可以看一下这篇文章,就不在本文进行探讨。

    • 原型链其实没有什么神秘的,他本质就是对其他对象的引用

    • 当在原本的对象中找不到属性时,就往引用的其他对象里面找。

    • 所有普通的原型链都会指向内置的Object.prototype,所以这个Object.prototype对象里,包含着JavaScript中许多常用的功能。而这个对象的原型链的尽头则是指向null

大多数浏览器支持使用__proto__ 来获取[[prototype]]__proto__ 的读取器(getter)暴露了一个对象的内部 [[Prototype]] ,但该特性已经从 Web 标准中删除,虽然一些浏览器目前仍然支持它,但也许会在未来的某个时间停止支持,请尽量不要使用该特性,在后续的ES标准中,提供相关的API可以使用。

  • 对一个函数来说,它其实也是一个对象,它区别于普通对象来说,最主要的区别是多了一个prototype 属性,称为原型,原型的作用就是共享方法。
    • 里面一个默认的constructor 属性,用于记录实例是由哪个函数创建;
    • 当该函数被new一个实例时,实例对象的原型链则指向它,所以我们可能会在函数的prototype中定义所谓公共方法。

new也是一个语法🍬 ,在这篇文章中,也描述了它的执行过程。

「你必须知道的JavaScript」到底“这”是什么啊 - 掘金

属性设置和屏蔽

在上一篇文章我们提到了[[Put]],我们来讲讲当原对象不存在该属性时,[[Put]]进行的操作:

  • 遍历原型链,如果还是找不到同名属性,那么直接在该对象中添加该属性

  • 如果在原型链上能找到该属性呢,则会进行下面的判断:

    • 如果该属性没有被标记为只读(writable:false),那么直接添加到对象本身

    • 如果该属性被标记为只读(writable:false),那么忽略该语句,不会发生屏蔽。在严格模式下会报错

    • 如果该属性是一个setter,那么会调用这个setter,不会发生屏蔽,也不会重新定义。

new一个函数

在JavaScript中,我们把带new的函数调用,称之为构造函数调用;当我们使用new时,在这过程中会执行[[prototype]]链的链接。我们来试试:

javascript
function Bird() {
  console.log('Bird');
}
console.dir(Bird)
let bird = new Bird();
console.dir(bird);

我们会发现,原型链上的constructor指向Bird(),那么有人会认为bird由Bird构造,因为constructor指向Bird。

可当我把Bird的prototype修改了,实例化对象会有什么变化

javascript
function Bird() {
  console.log('Bird');
}
Bird.prototype = {};

let bird = new Bird();
console.dir(bird);

通过这两张图,我们验证了bird实例的原型链指向其“构造函数”的prototype上。

同时,我们也知道prototype上的constructor默认指向该函数,但prototype是可变的,当我们对其进行改变,表面上bird还是Bird构造的,但是bird.constructor并非指向Bird,而是Object。(事实上默认的constructor已经被摧毁,当前指向的是 原型链上一层的constructor)

因此,靠constructor来判断其由谁构造是不可取的! 我们也可以理解到,在JavaScript中并没有构造函数可言,因为new函数调用就变成一个构造函数调用。

一般来说,是不允许直接改变原型prototype的指向。

在ES6中,提供Object.setPrototypeOf()来修改关联。

例题

js
// a
function Foo () {
 getName = function () {
   console.log(1);
 }
 return this;
}
// b
Foo.getName = function () {
 console.log(2);
}
// c
Foo.prototype.getName = function () {
 console.log(3);
}
// d
var getName = function () {
 console.log(4);
}
// e
function getName () {
 console.log(5);
}

Foo.getName();           // 2
getName();               // 4
Foo().getName();         // 1
getName();               // 1 
new Foo.getName();       // 2
new Foo().getName();     // 3
new new Foo().getName(); // 3

类思想中的继承

在ES6之前,我们可以通过构造函数+原型对象模拟实现。

javascript
function Father(name) {
    this.name = name;
}
Father.prototype.dance = function () {
    return 'dance';
};
function Son(name, age) {
    Father.call(this, name);
    this.age = age;
}
let son = new Son('merlin', 100);
son.dance();   //报错

如何继承父类的方法呢?

如果使用Son.prototype = Father.prototype,我们知道,原型是一个对象,用=操作符实际上是引用关系而不是复制。因此如果我们修改子类的原型方法,父类的原型同样会受到影响。

因此我们可以将子类的原型指向父类的实例,会生成一个新对象,并且原型链会指向父类的原型,这样子既可以共享父类的方法,为子类增加方法,父类也不会受影响。

javascript
Son.prototype = new Father();
Son.prototype.singAndDance = function () {
  console.log(this.name + ' sing and' + this.dance());
};

ES6之后,引入Class语法🍬 ,使代码更具备可读性。

class的引入,只是摒弃了之前那种丑陋的语法,其本质上还是通过[[prototype]]机制实现的。实现还是“类”的思想。

javascript
class Father {
  constructor(name) {
    this.name = name;
  }
  dance() {
    return 'dancing';
  }
}
class Son extends Father {
  constructor(name, score) {
    super(name);
    this.score = score;
  }
  singAndDance() {
    console.log(this.name + ' sing and' + this.dance());
  }
}
  • class必须被new调用,否则会报错。这是他与普通构造函数的一个主要区别,后者不需要new也可以执行。

  • 类的所以实例共享一个原型变量。

  • 类的内部,默认是严格模式

更简洁的设计

我们可以基于行为委托机制,使这个过程变得更直接,明了。

javascript
let father = {
  init(name) {
    this.name = name;
  },
  dance() {
    return 'dancing';
  },
};

let son = {
  singAndDance() {
    console.log(this.name + ' sing and' + this.dance());
  },
};

Object.setPrototypeOf(son, father);
son.init('son')
son.singAndDance()

说到底,基于类的实现到最后创建出来的也是对象,只不过将过程抽象成父类子类的形式。当使用对象来进行设计代码时,语法更加简明,结构更加清晰。

行为委托认为,对象之间是兄弟关系,互相委托,而不是父类和子类的关系,JavaScript中的[[prototype]]本质上就是行为委托机制。

对象关联倡导直接创建和关联对象,不把它们抽象成类。对象关联可以基于[[prototype]]的行为委托非常自然的实现

——《你不知道的JavaScript》上册

原型链梳理

js
function Person(name) {
    this.name = name
}
var p2 = new Person('king');
console.log(p2.__proto__) //Person.prototype
console.log(p2.__proto__.__proto__) //Object.prototype
console.log(p2.__proto__.__proto__.__proto__) // null
console.log(p2.__proto__.__proto__.__proto__.__proto__)//null后面没有了,报错
console.log(p2.__proto__.__proto__.__proto__.__proto__.__proto__)//null后面没有了,报错
console.log(p2.constructor)//Person
console.log(p2.prototype)//undefined p2是实例,没有prototype属性
console.log(Person.constructor)//Function 一个空函数
console.log(Person.prototype)//打印出Person.prototype这个对象里所有的方法和属性
console.log(Person.prototype.constructor)//Person
console.log(Person.prototype.__proto__)// Object.prototype
console.log(Person.__proto__) //Function.prototype
console.log(Function.prototype.__proto__)//Object.prototype
console.log(Function.__proto__)//Function.prototype
console.log(Object.__proto__)//Function.prototype
console.log(Object.prototype.__proto__)//null

MIT Licensed | Copyright © 2021 - 2022