小宋爱睡觉 小宋爱睡觉
首页
  • HTML
  • CSS
  • JavaScript
  • Vue
  • React
  • 计算机网络
  • 浏览器原理
  • 性能优化
  • 设计模式
手写系列
  • 字符串
  • 数组
  • 链表
  • 树
  • 动态规划
  • 排序算法
  • GitHub (opens new window)
  • JueJin (opens new window)
首页
  • HTML
  • CSS
  • JavaScript
  • Vue
  • React
  • 计算机网络
  • 浏览器原理
  • 性能优化
  • 设计模式
手写系列
  • 字符串
  • 数组
  • 链表
  • 树
  • 动态规划
  • 排序算法
  • GitHub (opens new window)
  • JueJin (opens new window)
  • 数据类型
  • ES6
  • JS基础
  • 执行上下文/ 闭包/ 作用域链
  • 原型和原型链
    • 原型与原型链
    • 原型修改和重写致constructor丢失
    • 继承的几种方式
    • 原型链的终点是什么
    • 如何获得对象非原型链上的属性
    • ES5的继承和ES6的继承有什么区别?
    • 几道原型链的题
  • 情景题
  • JavaScript
Crucials
2021-11-28
原型与原型链
原型修改和重写致constructor丢失
继承的几种方式
原型链的终点是什么
如何获得对象非原型链上的属性
ES5的继承和ES6的继承有什么区别?
几道原型链的题

原型和原型链

# 原型与原型链特别重要

概念

在js内部使用构造函数来新建一个对象,每一个构造函数里面都有一个prototype属性,他的值是一个对象,包含了由该构造函数的所有实例共享属性和方法,当使用构造函数新建一个对象时。对象内部将包含指针 指向构造函数的prototype属性对应的值,获取prototype可以用Object.getPrototypeOf()

笔记

proto是隐式原型,一个对象的隐式原型指向构造该对象的构造函数的原型,这也保证了实例能够访问在构造函数原型中定义的属性和方法,ES5中有了对于这个内置属性标准的Get方法Object.getPrototypeOf().

但是Object.prototype 这个对象是个例外,它的__proto__值为null

作用是构成原型链,同样用于实现基于原型的继承。举个例子,当我们访问obj这个对象中的x属性时,如果在obj中找不到,那么就会沿着__proto__依次查找

prototype是显式原型,每一个函数在创建之后都会拥有一个名为prototype的属性,这个属性指向函数的原型对象。

但是通过Function.prototype.bind方法构造出来的函数是个例外,它没有prototype属性。

作用是用来实现基于原型的继承与属性的共享。

两者的关系是隐式原型指向创建这个对象的函数(constructor)的prototype

笔记

constructor是返回创建该对象的函数的引用。

简单来说就是找到这个对象是通过什么构造函数来生成它的,通过constructor就能找到这个函数

function Person(area) {
  this.type = "person";
  this.area = area;
}
Person.prototype.sayArea = function () {
  console.log(this.area);
};
var Father = function (age) {
  this.age = age;
};
Father.prototype = new Person("Beijin");
console.log(Person.prototype.constructor); //function person()
console.log(Father.prototype.constructor); //function person()
Father.prototype.constructor = Father; //修正
console.log(Father.prototype.constructor); //function father()
var one = new Father(25);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

我们可以看出如果是单纯function(){}出来则它的constructor是最顶层的Function,但是假设他是new出来或者继承了原型的话,他会先找自身的constructor,找不到的话依次去原型链上找constructor,这里为了修正,可以指回去,但是假如不指回去的话也有应用,我们写一个插件,别人得到的是你实例化的对象,如果别人想扩展一下对象的话,可以通过instance.prototype.constructor去继承来的原型上修改或者新增共享的方法

# 原型修改和重写致constructor丢失

function Person(name) {
	this.name = name
}

// 修改原型
Person.prototype.getName = function() {}
var p = new Person('Hello')
console.log(p.__proto__ === Person.prototype) // true 最终都指向Object
console.log(p.__proto__ ==== p.constructor.prototype) // true p的constructor是Person 因此为true

// 重写原型
Person.prototype = {
	getName: function() {}
}

var p = new Person('Hello')
// p.constructor = Person 这里指回来
console.log(p.__proto__ === Person.prototype) // true 最终都指向Object
console.log(p.__proto__ ==== p.constructor.prototype) // false 给Person的原型对象用对象赋值是,构造函数指向了根构造函数Object 想变成true就得指回来 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 继承的几种方式特别重要

  • 原型链继承
function Parent() {
  this.name = 'crucials'
}

Parent.prototype.getName = function() {
  return this.name;
}

function Child() {}

// 这里也可以直接写出Child.prototype = Parent.prototype
// 但是这样就不能访问到父类的构造函数的属性了,即this.name
Child.prototype = new Parent()

var child = new Child()
child.getName() // crucials
// 优点:逻辑简单
// 父类构造函数中的引用类型(比如对象/数组),会被所有子类实例共享。
// 其中一个子类实例进行修改,会导致所有其他子类实例的这个值都会改变
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  • 构造函数继承
function Parent() {
  this.name = ['crucials']
}

function Child() {
  Parent.call(this)
}

var child = new Child()
child.name.push('fe')

var child2 = new Child() // child2.name === ['crucials']
// 优点:解决了原型链继承中构造函数引用类型共享的问题,同时可以向构造函数传参(通过call传参)
// 缺点:所有方法都定义在构造函数中,每次都需要重新创建(对比原型链继承的方式,方法直接写在原型上,子类创建时不需要重新创建方法)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • 组合继承
function Parent() {
  this.name = 'crucials'
}

Parent.prototype.getName = function() {
  return this.name
}

function Child() {
  Parent.call(this)
  this.topic = 'fe'
}

Child.prototype = new Parent()
// 需要重新设置子类的constructor,Child.prototype = new Parent()相当于子类的原型对象完全被覆盖了
Child.prototype.constructor = Child

// 优点:同时解决了构造函数引用类型的问题,同时避免了方法会被创建多次的问题
// 缺点:父类构造函数被调用了两次。同时子类实例以及子类原型对象上都会存在name属性。虽然根据原型链机制,并不会访问到原型对象上的同名属性,但总归是不美。
// 具体是Parent.call调用了一次父类构造函数,Child.prototype = new Parent()又调用了一次父类构造函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  • 寄生组合继承
function Parent() {
	this.name = 'crucials'
}
Parent.prototype.getName = function () {
	return this.name
}
function Child() {
	Parent.call(this)
}
Child.prototype = Object.create(Parent.prototype)//核心代码
Child.prototype.constructor = Child//核心代码
var s1 = new Child()
console.log(s1 instanceof Child, s1 instanceof Parent) // true true
console.log(s1.constructor) //Child

// 解决上面全部缺点
// 暂时无缺点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

问题1传入的为什么是Parent.prototype而不是Parent,问题2为什么Object.create可以不二次调用自身实例上方法

问题一先参考一下Object.create的实现,然后会发现他是创建了一个函数,往函数的原型上面添加传入的对象,假如传入的是Parent的话他是一个函数,而prototype是对象,自然会出现奇奇怪怪的错误导致访问不到原型上面的属性,解决了问题一

而问题二Object.create不会调用父类构造函数是因为他只继承prototype,并没有调用构造函数,相反new他在改变this之后还会执行一下构造函数,问题二解决

  • ES6
class Parent {
  constructor() {
    super()
    this.name = 'crucials'
  }

  getName() {
    return this.name
  }
}

class Child extends Parent {
  constructor() {
    // 这里很重要,如果在this.topic = 'fe'后面调用,会导致this为undefined,具体原因可以详细了解ES6的class相关内容,这里不展开说明
    super()
    this.topic = 'fe'
  }
}

const child = new Child()
child.getName() // crucials
//注意es6的class可以看成是上面栗子中是parent.call(this, value)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 原型链的终点是什么

Object.prototype.__proto__ = null

# 如何获得对象非原型链上的属性

使用obj.hasOwnProperty(key)

# ES5的继承和ES6的继承有什么区别?重要

ES5的继承时通过prototype或构造函数机制来实现。ES5的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到this上(Parent.apply(this))。

ES6的继承机制完全不同,实质上是先创建父类的实例对象this(所以必须先调用父类的super()方法),然后再用子类的构造函数修改this。

具体的:ES6通过class关键字定义类,里面有构造方法,类之间通过extends关键字实现继承。子类必须在constructor方法中调用super方法,否则新建实例报错。因为子类没有自己的this对象,而是继承了父类的this对象,然后对其进行加工。如果不调用super方法,子类得不到this对象。

值得注意的是

super关键字指代父类的实例,即父类的this对象。在子类构造函数中,调用super后,才可使用this关键字,否则报错。

# 几道原型链的题

Function.prototype.a = () => console.log(1);
Object.prototype.b = () => console.log(2);
function A () {};
var a = new A();

A.a(); // 1
a.a(); // Uncaught TypeError: a.a is not a function at <anonymous>:1:3
a.b(); // 2
1
2
3
4
5
6
7
8
  • A.a() 在A中找,A里面没有,就根据原型链往上找,A.prototype上没有,但A是Function的实例,Function.prototype中有a属性,所以打印1.
  • new 关键字,除了Function这个内置的构造器之外构建的实例的类型都是对象;对象a里没有a属性,只能往上找,a的原型链中到Object.prototype没有a属性,只有b属性,所以a.a()就会报错,a.b() 就会打印2。
function A() {
  B = function() {
    console.log(10)
  }
  return this
}

A.B = function() {
  console.log(20)
}

A.prototype.B = function() {
  console.log(30)
}

var B = function() {
  console.log(40)
}

function B() {
  console.log(50)
}

// questions
A.B() // 20
B() // 40
A().B() // 10
B() // 10
new A.B() // 20
new A().B() // 30
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

先不管下面的问题,执行变量提升后 输出40的已经把50的给覆盖掉了,下面看题

  1. A.B执行第二个函数 输出20
  2. B执行第四个函数 输出40
  3. A()最后返回this,因为A函数在全局作用域,所以this是window,执行B函数,因为A函数里面的B没有关键字声明,泄露到全局作用域中,覆盖了之前的B函数,所以输出10
  4. 输出10
  5. new会匹配离他最近的括号,A.B是20 那 new A.B()也是20
  6. 先new A 在执行里面的B函数,注意new之后只会继承他实例上和原型上的属性,不会继承例如 A.B = xxx 故输出30
上次更新: 2022/03/20, 19:40:28
执行上下文/ 闭包/ 作用域链
情景题

← 执行上下文/ 闭包/ 作用域链 情景题→

Copyright © 2021-2025 粤ICP备2021165371号
  • 跟随系统
  • 浅色模式
  • 深色模式