原型与原型链

1

avatar

  • 函数独有属性 prototype ,指向函数的原型对象(XX.prototype)
  • 所有对象拥有属性 proto 和 constructor 。函数也是对象的一种。
  • JS中有7种基础数据: string, number, bigint, boolean, undefined, symbol, 和null类型。
    除此外,其他所有都是Object类型,也就是对象。
  • 原型对象(XX.prototype)的constructor属性指向自定义的构造函数,或内置的构造函数,JavaScript中有许多内置构造函数,比如Function(),Object()。
  • 除了基础数据类型,其他皆对象,所以构造函数也是对象,对象上可以挂载方法,即静态方法,如:Object.entries,Array.isArray等。

2

avatar

prototype

函数独有的属性,从一个函数指向另一个对象,
代表这个对象是这个函数的原型对象,这个对象也是当前函数所创建的实例的原型对象。
prototype设计之初就是为了实现继承,让某一个构造函数实例化的所有对象可以找到公共的方法和属性。
有了prototype我们不需要为每一个实例创建重复的属性方法,而是将属性方法创建在构造函数的原型对象上(prototype)。

proto

__proto__属性是对象(包括函数)独有的。
每个对象都有__proto__属性,从一个对象指向该对象的原型对象(也可以理解为父对象)。
这样,实例就能访问到构造函数原型上的方法和属性了

tips: hasOwnProperty 可以判断某个属性是对象自己的 还是从原型链上继承来的

原型链的查找机制

当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。
如果没有就查找它的原型对象(也就是 __proto__指向的 构造函数的prototype)。
如果还没有就查找原型对象的原型(Object的原型对象 也即Object.prototype)。
依此类推一直找到 Object 为止(null)。
__proto__的意义就在于给查找机制提供一条路线。

constructor

constructor是对象才有的属性,它从一个对象指向一个函数的。(即对象的构造函数)
这就是constructor的作用: 从一个对象指向一个函数,这个函数就是该对象的构造函数。
比如: p1的constructor属性指向了Parent,那么Parent就是p1的构造函数;
Parent的constructor属性指向了Function,那么Function就是Parent的构造函数;
Function函数的构造函数就是本身了,
也就可以说Function是所有函数的根构造函数。

类式继承(classical inheritance)

  • 实现本质
    用父类的实例,重写子类的原型。
  • 核心代码
    Son.prototype = new Parent();
  • 缺陷
    原型属性中的引用类型属性,会被所有实例共享,更改一个子类实例,会影响其他子类。

构造函数式继承

  • 实现本质
    在子类的构造函数里,调用call/apply来实现继承。
  • 核心代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Parent(username, password) {
this.password = password;
this.username = username;
Parent.prototype.login = function () {
console.log(this.username + '密码是' + this.password);
}
}


function Son(username, password) {
Parent.call(this, username, password);//通过call向父类的构造函数传递参数
this.articles = 3; // 文章数量
}


const s1 = new Son('foo', '1234');
// 没有继承父类原型上的方法。
console.log(s1.login()); // TypeError: s1.login is not a function
  • 缺陷
    没有继承父类原型上的方法。

组合式继承

  • 实现本质
    类式继承继承方法,构造函数继承属性
  • 缺陷
    父类的构造函数被调用了两次,显得多余;

js实现继承————寄生组合式继承

  • 什么是继承
    继承是指子类拥有父类的属性和方法,使代码可以复用。
    ES6的class、extends是一种语法糖,
    实现继承,本质仍是基于原型和原型链。

  • 实现思路

  1. 在子类构造函数内,用call()调用父类的构造函数,并绑定this;(继承的属性没有被创建在原型链上,因此多个子类不会共享同一个属性)

  2. 借助中间函数F实现原型链继承,(封装在inherit函数); (继承父类的方法)

  3. 在子类的构造函数的原型上定义新方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

//寄生组合式继承的核心方法
function inherit(child, parent) {
// 继承父类的原型
const p = Object.create(parent.prototype);
// 重写子类的原型
child.prototype = p;
// 重写被污染的子类的constructor
child.prototype.constructor = child;
}

//User, 父类
function User(username) {
this.username = username; // 实例基本属性 (该属性,强调私有,不共享)
this.arr = [1]; // (该属性,强调私有)
}

// 需要复用、共享的方法定义在父类原型上
User.prototype.login = function () {
console.log(this.username + ':' + _password);
}

//WebUser, 子类
function WebUser(username, password) {
User.call(this, username, password) // 继承属性
this.articles = 3 // 文章数量
}

/// 实现原型继承
inherit(WebUser, User);

//在子类原型上添加新方法
WebUser.prototype.readArticle = function () {
console.log('Read article');
}

若不使用es5的 Object.create();
inherit方法可如下,
其中: 函数F仅用于桥接,仅创建了一个new F()实例

1
2
3
4
5
6
function inherit(child, parent) {
var F = function () {};
F.prototype = parent.prototype;
child.prototype = new F();
child.prototype.constructor = child;
}

ES6 中 class 的继承​

ES6 中引入了 class 关键字,通过 extends 关键字实现继承,还可以通过 static 关键字定义类的静态方法。
需要注意的是:class 关键字只是原型的语法糖, JavaScript 继承仍然是基于原型实现的。

tips: 在子类的构造函数里,通过 super 调用父类的构造方法

new 操作符都做了什么

new 操作符用于创建一个给定构造函数的实例对象。

  • 在内存中创建一个新对象obj
  • 将新对象内部的 __proto__ 赋值为构造函数的 prototype 属性。
  • 将构造函数内部的 this 指向新对象
  • 执行构造函数内部的代码(给新对象添加属性)
  • 如果构造函数返回非空对象,则返回该对象。否则返回 this
1
2
3
4
5
6
7
8
9
10
function myNew(Func, ...args) {
// 1.创建一个新对象
const obj = {}
// 2.新对象原型指向构造函数原型对象
obj.__proto__ = Func.prototype
// 3.将构造函数内的this指向新对象
let result = Func.apply(obj, args)
// 4.根据返回值判断
return result instanceof Object ? result : obj
}
function Person(name, age) {
  this.name = name;
  this.age = age;
  this.eat = function() {
    console.log(age + "岁的" + name + "吃饭");
  }
}

Person.run = function () {}
Person.prototype.walk = function () {}

let p1 = new Person("lily", 20);
let p2 = new Person("bob", 30);

console.log(p1.eat === p2.eat); // false
console.log(p1.run === p2.run); // true
console.log(p1.walk === p2.walk); // true

false; true; true

new 操作符使构造函数内的 eat 函数(对象),是开辟一个新的空间存放,构造函数内的this指向实例化对象,所以两个实例对象它不共享。

而原型上两个实例对象自然都是同一份,walk 方法相同。

p1.run 和 p2.run 都是 undefined。因为 run 方法只是作为 Person 自己的静态属性,p1 和之后的原型链上是找不到的。

查看评论