new 构造函数都做了一些什么?

2020/01/04 JavaScript 共 5939 字,约 17 分钟

原型链的理解

在JavaScript中,每个对象都有一个内部属性__proto__(原型),代表该对象的原型。这个原型又有自己的__proto__, 这样就形成了一个链式连接的原型链。原型链的本质是一个普通的对象之间的关系。实现原型链的本质为:如果要访问一个对象/实例的属性/方法,首先在自身内部查找,如果找到了就使用自身的属性/方法,如果没找到,就去原型( __proto__指向的那个对象)中查找,如果找到了就使用原型中的属性/方法,依次递推,直到顶层Object.prototype。Object.prototype是原型链的顶端,它自身的原型为null。

在使用构造函数创建对象时,会为该对象创建对应的原型对象,该原型对象的__proto__指向构造函数的prototype。这样,实例中未定义的方法或属性,就可以在原型链中查找到对应的值。如果一个方法或属性在原型链中的某个位置被定义,则该方法或属性会在实例中得到复制,可以直接访问。

通过原型链,可以实现面向对象编程中的继承、重载和对象共享等功能。

函数,原型与继承

构造函数的 prototype 指向实例原型,实例原型的 constructor 指向构造函数(它是对这个函数本身的一个引用)。函数的 prototype 是一个普通的对象。这个对象具有一个属性:constructor。它是对这个函数本身的一个引用

实例的 __proto__ 指向实例原型。(和 Object.getPrototypeOf(obj) 一个意思// 返回指定对象的原型)

因为一个构造函数可以 new 很多个实例,所以实例和构造函数之间就没有指向关系了

原型是定义属性和功能的一种便捷方式,对象可以访问原型上的属性和功能,对象的原型属性是内置属性(使用标记[[prototype]]),无法直接访问

继承是代码复用的一种方式,继承有助于合理地组织程序代码,将一个对象的属性扩展到另一个对象上

函数的prototype 和实例的__proto__和实例原型的constructor 关系

对象是没有类型的,他对新的名字和属性的值没有限制,在对象初始化的时候,尽可能的减少初始化的东西,可以减少初始化的时间和内存

每一个函数都有一个 prototype 他指向了实例原型( prototype 是函数才会有的属性)。

每一个实例原型都有一个 constructor 属性,这个实现指向函数本身。

每一个实例都有一个 __proto__ 属性,指向了实例原型。

根据对应关系进行对比

function Person() {

}
var person = new Person();

person.__proto__===Person.prototype
true

person.__proto__.constructor===Person
true

person.constructor===Person
true

注意:后文中提到的 函数的原型 其实就是实例原型。 当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。

如果直接是对象的,他就没有 prototype 属性,他的 __proto__ 属性,指向他的原型(即对象),他的 constructor 属性,这个实现指向函数本身

到这里我们理清楚了函数和对象的关系,prototype__proto__ 还有 constructor 分别是谁的属性,干什么的应该也差不多了。

function Person() {

}
var person = new Person();
console.log(person.constructor === Person); // true

当获取 person.constructor 时,其实 person 中并没有 constructor 属性,当不能读取到 constructor 属性时,会从 person 的原型也就是 Person.prototype 中读取,正好原型中有该属性,所以:

person.constructor === Person.prototype.constructor

新创建的对象,我们想要他指定到某一个原型可以这样做:object.create({},obj) ,他只能用来创建对象,创建数组是没有意义的

继承与原型链

说一说 new 构造函数都发生些什么

创建一个空对象 => 改变隐式原型的指向 => 改变this的指向,指向构造函数,为这个对象添加属性 => 返回这个新对象

一个Function 对象在使用new运算符来作为构造函数时,会用到他的prototype属性,它将成为新对象的原型,即对象的原型等于构造函数的prototype,但是不是所有的Function对象都拥有prototype属性(比如生成器函数,Symbol() 和 BigInt() 函数会在它们通过 new 运算符来调用时抛出,因为 Symbol.prototype and BigInt.prototype 只是用来为原始值提供方法的,这时不应该直接构建包装器对象。

构造函数的目的是创建一个新对象,并进行初始化设置,然后将其作为构造函数的返回值,在构造函数内部,关键字this指向新创建的对象,所以在构造器内添加的属性直接在新的ninja实例上

理清楚 prototype__proto__ 还有 constructor 是为了更好的理解构造函数准备。在一次模拟面试中,朋友问我通过 new 关键字调用函数都发生了一些什么?

关于构造函数的概念,简单介绍,函数声明的时候首字母大写(为了区分普通函数和构造函数),调用的时候通过 new 关键字,否则和普通函数一样,没有区别。

prototype__proto__ 还有 constructor

如果是构造函数调用的话

通过打印 this 发现他指向了函数的原型,推导出 this.__proto__=Persion.prototype

在顺便说一句 构造函数,实例,和原型对象的关系

通过关键字new调用JavaScript构造函数。因此,每次调用构造函数时,都会创建一个新的词法环境,该词法环境保持构造函数内部的局部变量

由于每次调用函数时均会创建新的执行上下文,因此创建了新的getFeints执行环境并推入执行栈。这同时引起创建新的词法环境,词法环境通常用于保持跟踪函数中定义的变量。另外,getFeints词法环境包含了getFeints函数被创建时所处的环境,当ninja2对象构建时,Ninja环境是活跃的。

new 的模拟实现

new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。

模拟实现:

  1. 创建一个新的对象

  2. 把新对象的原型指向传进来的参数

  3. 执行函数

  4. 根据条件返回结果(如果构造函数返回一个对象,则该对象将作为整个表达式的值返回,而传入构造函数的this将被丢弃.但是,如果构造函数返回的是非对象类型,则忽略返回值,返回新创建的对象)

// 实现方式1
function myNew(fn, name) {
  let obj = Object.create(fn.prototype)
  let res = fn.call(obj, name)
  return (typeof res === 'object' || typeof res === 'function') ? res : obj
}
// 实现方式2
function myNew(fn, name) {
  let obj = {}
  Object.setPrototypeOf(obj, fn)
  let res = fn.call(obj, name)
  return (typeof res === 'object' || typeof res === 'function') ? res : obj
}
// 实现方式3
function myNew(fn, name) {
  let obj = {}
  obj.__proto__ = fn.prototype
  let res = fn.call(obj, name)
  return (typeof res === 'object' || typeof res === 'function') ? res : obj
}

function Person(name) {
  this.name = name
}
let n = myNew(Person, 'thj')
console.log(n)

Object.create()

构造函数的目的是根据初始条件对函数调用创建的新对象进行初始化

class 实现原理

classes6 的语法糖,他的底层还是通过函数实现的

class Person {
  constructor() {
    this.name="sunseekers"
  }
  say(){
    return `${this.name} say: ever weeping, ever youthful`
  }
  static info(age,address){
    return `${age} and ${address}`
  }
}

// 等价于
function Person(name){
  this.name=name
}
Person.prototype.say=function(){
  return `${this.name} say: ever weeping, ever youthful`
}
Person.info=(age,address)=>`${age} and ${address}`

类的数据类型就是函数,类本身就指向构造函数

class Point {
  // ...
}

typeof Point // "function"
Point === Point.prototype.constructor // true

原型方法和工具方法区别

instanceof 运算符的第一个参数是一个对象,第二个参数一般是一个函数

instanceof 的判断规则是: 沿着对象的 __proto__ 这条链来向上查找找,如果能找到函数的prototype 则返回 true ,否则 返回 false

function Person(name, age) {
  this.name = name;
  this.age = age;
  // 构造函数方法,每声明一个实例,都会重新创建一次,属于实例独有
  this.getName = function() {
    return this.name;
  }
}

// 原型方法,仅在原型创建时声明一次,属于所有实例共享
Person.prototype.getAge = function() {
  return this.age;
}

// 工具方法,直接挂载在构造函数名上,仅声明一次,无法直接访问实例内部属性与方法
Person.each = function() {
 console.log(7887)
}
// new Person.each()或者Person.each()可以执行这个方法

一个属性如果放在原型上的话,是可能通过实例来调用的

但是放在类上的,不能通过实例来调用,只能用过类名来调用

在函数的原型上创建对象方法是很有意义的,这样我们可以使得同一个方法由所有对象实例共享,没必要写在每个函数里面,这样避免new 很多实例的时候生成很多没有必要的方法

如果我们需要私有对象,在构造函数内指定方法是唯一的解决

class 和 new 混用

es6 普遍之后,大部分的时候都是使用 class 创建一个类,几乎很少使用构造函数去创建一个类。因为使用起来方便简洁,具体对比不详细介绍

问一个问题: class 创建的类能在构造函数创建的类中使用调用吗?反之成了吗?

function UserInfo() {
  this.name = 'sunseekers'
  this.color = 'red'
}

class Persion {
  constructor() {
    this.name = 'kitty'
    this.color = 'yellow'
    this.info = new UserInfo()
  }
  getNames() {
    console.log('name:' + this.name)
    console.log(new UserInfo())
  }
}

答案是,调用恰当,是可以互相调用的。

补充原型知识

JavaScript动态特性他有他的副作用

通过原型,一切都可以在运行时修改

结论:函数的原型可以被任意替换,已经构建的实例引用旧的原型,替换后构建的实例指向新的原型。对象与函数原型之间的引用关系是在对象创建时建立的。新创建的对象将引用新的原型,它只能访问pierce方法,原来旧的对象保持着原有的原型,仍然能够访问swingSword方法。

constructor属性可用于检测一个对象是否由某一个函数创建的

实例原型的constructor属性的存在仅仅是为了说明该对象是从哪儿创建出来的。如果重写了constructor属性,那么原始值就被丢失了。避免constructor丢失我们可以手动引用到原来的

因为javascript的动态性,instanceof操作符检测的结构也未必是对的,在程序的执行过程中,我们可以修改很多内容

function Name(){}
const myName = new Name()
console.log(myName instanceof Name) // true
Name.prototype={}
console.log(myName instanceof Name) // false 因为原型发生了改变

第一个测试正常。但是如果我们在ninja实例创建完成之后,修改Ninja构造函数的原型,再执行测试ninja instanceof Ninja,我们会发现结果发生了变化。instanceof操作符真正的的语义——检查右边的函数原型是否存在于操作符左边的对象的原型链上,构造函数的原型发生了变化,检测也会跟着变化

综上所诉:小心函数的原型可以随时发生改变,在JavaScript这种动态类型语言中,为了安全需要付出性能开销

混合类的继承

class Base{
  base(){
    console.log('base')
  }
}

const A=(base)=>class extends base{
  a(){
    console.log('a')
  }
}
const B=(base)=>class extends base{
  b(){
    console.log('b')
  }
}
const C=(base)=>class extends base{
  c(){
    console.log('c')
  }
}

class M extends C(B(A(Base))){
  m(){
    console.log('m')
  }
}
let more = new M() // more 会继承前面所有的

function mix(baseClass,...mixClass){
  return mixClass.reduce((prev,next)=>next(prev),baseClass)// 
}

参考文章

JavaScript 深入之从原型到原型链

JavaScript 深入之 new 的模拟实现

Class 的基本语法


在技术的历史长河中,虽然我们素未谋面,却已相识已久,很微妙也很知足。互联网让世界变得更小,你我之间更近。

在逝去的青葱岁月中,虽然我们未曾相遇,却共同经历着一样的情愫。谁的青春不曾迷茫或焦虑亦是无奈,谁不曾年少过

在未来的日子里,让我们共享好的文章,共同学习进步。有不错的文章记得分享给我,我不会写好的文章,所以我只能做一个搬运工

我叫 sunseekers(张敏) ,千千万万个张敏与你同在,18年电子商务专业毕业,毕业后在前端搬砖

如果喜欢我的话,恰巧我也喜欢你的话,让我们手拉手,肩并肩共同前行,相互学习,互相鼓励

文档信息

Search

    Table of Contents