用 new 把JavaScript中的知识点串起来

通过本篇文章,你可以学习|复习下面的知识点:

  • new运算符的使用

  • 数据类型、原始值

  • 内置的构造函数、自定义构造函数

  • 原型、原型链

  • ES6 Class

new 运算符

new用于创建具有构造函数的对象类型的实例。语法如下:

new Constructor[([args])];
 

其中,Constructor即构造函数,构造函数的参数args是可选的;当没有args时,new Constructornew Constructor()是等同的。

执行 new时,大致做了这些事情:

  1. 创建一个空对象{}

  2. 将空对象的__proto__指向Constructor.prototype(实现类型的继承,构建原型链)

  3. 将创建的对象作为构造函数中的this,执行构造函数

  4. 构造函数没有return的情况下返回构造函数内部的this,这就是 new运算符的运算结果(你也可以在构造函数中主动选择返回对象,来覆盖正常的对象创建步骤,但这不是建议的行为——在TypeScript中明确要求构造函数返回类型必须为void

因此,如果对一个空函数进行new运算,将返回一个空对象{}

new (function() {}); // {}
 

对于构造函数,执行new Constructor()Constructor()得到的结果通常也是不一样的:

typeof Date(); // 'string'
typeof new Date(); // 'object' 
 

new除了可以对构造函数进行运算,还可以对ES6中的class进行运算。但ES6的class只是一个语法糖,本质上还是构造函数,因此先让我们把重点放在构造函数

内置构造函数

内置的构造函数有很多很多,日常使用的有:Date()Promise()Map()Set()

在JavaScript中,约定构造函数使用大驼峰方式命名。

我们可以将构造函数的名称,作为该构造函数生产出来的实例的类型,如new Date()类型为Date,new Error('出错了出错了啊')类型为Error,[]类型为Array,{}类型为Object,100类型为Number,'nice !'类型为String,true类型为Boolean……对于构造函数的名称,我们可以通过Constructor.name获取到。(后面借助这一点,实现获取任意对象的类型)

数据类型对应的构造函数

八大数据类型中,除了nullundefined,其余六种数据类型都有相应的构造函数的,分别是:

  • Object()

  • Boolean()

  • Number()

  • String()

  • Symbol()

  • BigInt()

后两个Symbol()BigInt()是在ES6中新增的基础类型,它们无法被new运算,只能通过调用构造函数的方式,获得一个原始值,如:const typeSymbol = Symbol('type');

之所以无法被new运算,是因为在ES6中规定:不能对基础类型的构造函数执行new运算。而Boolean()Number()String()还支持new运算,更多是为了兼容性考虑。现在推荐使用字面量方式创建原始值。

如果真的想获得一个原始值包装器(含有[[PrimitiveValue]]的对象),可以使用构造函数Object(),调用valueOf()方法即可获取到原始值:

image.png

new Object()Object()的效果几乎是一样的。

在JavaScript中,当我们说到“对象”或“object”时,通常指的是包含键值对的实例;当我们说到“Object”时,通常指的是构造函数Object()

原始值包装器

Boolean为例,执行new Boolean(0)

image_1.png

控制台中打印的Boolean {false}就是一个原始值包装器,它有两个特点:

  • 它是一个继承自Boolean.prototype的对象(该对象继承自Object.prototype),因此若执行new Boolean(false) && 100实际上返回的是100
  • 它包裹了一个值为false的有原始值(PrimitiveValue)

如果不使用运算符new只执行构造函数,将只返回原始值(有需要的话进行数据转换):

image_2.png

基本类型

七个基本类型:

  • Null: null

  • Undefined: undefined

  • Boolean: true false

  • Number: 1 100

  • String: 'hello' '你好'

  • Symbol: Symbol.toStringTag Symbol.toPromitive

  • BigInt: 1n 9999n

基本类型代表了最底层的语言实现。

基本类型的值被称为原始值、原始数据,如nullundefinedtrue100'hello'…都是原始值。

通过我们的日常开发可以发现,基本类型在代码中被大量使用,所以从一开始设计JavaScript这门语言时,基本类型必须要保证高效性

为了达到这一目标,原始值有如下特征:

  • 存放在栈中(复杂类型的引用值存放在堆中)

  • 不可变

  • 没有属性、方法

JavaScript中,除了原始值(基础类型的值),其他都是引用值(复杂类型的值,继承自Object.prototype),即使函数也是引用值,只不过函数内部有个[[callable]],可以执行()语法。

原始值是不可变的

let a = 1;
((num) => num++)(a);
a; // 1 
 

由于原始值不可改变的特性,将原始值作为参数传给函数时,其实是复制一份原始值的副本传入的,函数内部操作的这份副本,原本的原始值不受任何影响。

但如果函数接收一个引用值,它自身会随着函数体的操作而改变:

let arr = [];
((value) => value.push('Oh!'))(arr);
arr; // ["Oh!"]
 

原始值没有属性、方法

上面提到,为了保证高效性,原始值是没有属性和方法的,但是我们却经常进行如下操作:

const str = 'abc';
str.substr(-1); // 'c'
str.length; // 3 
 

为什么可以调用属性length,调用方法.substr()

这是因为对原始值str调用方法、属性时,其实是JavaScript引擎根据原始值,创建了对应的原始值包装器,即new String(str),然后在这个包装器上调用方法、属性。

同时由于原始值的不可变性,原始值包装器调用的所有方法,如.substr().substring().toFixed()等都不会改变原始值,函数运行结果作为一个全新的原始值被返回——这是所有原始值的特性。


讲完了new在JavaScript内置构造函数中的应用,再来看看其在自定义构造函数中的应用。

自定义构造函数

// 定义对象类型:Phone
function Phone(make, model) {
  this.make = make;
  this.model = model;
}
 

执行 new Phone('Apple', 'iPhone 12') 控制台输出:

image_3.png

这里我们创建了一个Phone类型的实例,属性makemodel都在执行构造函数时正常赋值了。

此外,该实例还有一个属性__proto__,它是什么?

__proto__

每个由new运算得到的实例都会有属性__proto__,它只用来做一件事:指向当前实例的原型(父类),即该实例的[[Prototype]]。上述例子中,它指向Phone.prototype。对于使用对象字面量创建的对象,它指向Object.prototype;对于使用数组字面量创建的对象,它指向Array.prototype,使用字符串字面量创建的原始值,它指向String.prototype

我们可以通过改变__proto__,以实现改变当前实例的原型,前提是该对象必须通过Object.isExtensible()判断为可扩展的。要变更的值必须是一个对象或null

因为性能缘故,__proto__已不被推荐使用,如果使用obj.__proto__ = ...极有可能出现问题!现在更推荐使用Object.getPrototypeOf(o)/Object.setPrototypeOf(o, proto)

那么,__proto__prototype的关系是?

prototype——原型

首先明确,prototype属性出现在哪些对象上?

答:内置的构造函数和自定义的普通函数

image_4.png

箭头函数没有prototype

image_5.png

实例也没有prototype

image_6.png

与之相应的,__proto__出现在对象实例上。

很多时候,我们发现原型也有__proto__,这是因为:原型也是某个其他原型的实例。没有多层继承的话,它通常是Object.prototype

Number.prototype.__proto__ === Object.prototype; // true
 

原型的两个基本属性

一个“纯净”的Constructor.prototype有两个属性:

  • constructor – 指向构造函数 Constructor

  • __proto__ – 原型Constructor.prototype__proto__通常指向 Object.prototype

constructor__proto__prototype的关系如图:

image_7.png

JavaScript中除了__proto__为空的对象,其他所有的对象都是Object的实例,都会继承Object.prototype的属性和方法——尽管它们可能被覆盖了。

有时候会故意创建不具有典型原型链继承的对象,比如通过Object.create(null)创建的对象,或通过obj.__proto__ = ... Object.setPrototypeOf(obj, proto)改变原型链。

改变Object原型,会通过原型链改变所有对象,这提供了一个非常强大的扩展对象行为的机制。下面代码通过扩展Object.prototype,使我们很方便的在程序中任何地方、获取任一对象的数据类型:

Object.defineProperty(Object.prototype, Symbol.type = Symbol('type'), {
  get() {
    // 规定 NaN 的类型为 'NaN',而不是 'Number'
    if (
      this.__proto__.constructor.name === 'Number' &&
      Number.isNaN(this.valueOf())
    ) {
      return 'NaN';
    }
    return this.__proto__.constructor.name;
  }
});
 

之后,除了nullundefined之外的所有基础类型数据、复杂类型数据,都可以通过调用[Symbol.type]属性获取其类型:

image_8.png

prototype在自定义构造函数中的应用

function Phone(make, model) {
  this.make = make;
  this.model = model;
  this.innerLogMake = function() {
    console.log('当前手机的厂商:', this.make);
  }
}

Phone.prototype.outerLogMake = function() {
    console.log('当前手机的厂商:', this.make);
}

const phone = new Phone('Apple', 'iPhone');

phone.innerLogMake(); // '当前手机的厂商: Apple'
phone.outerLogMake(); // '当前手机的厂商: Apple'
 

输出Phone.prototype

image_9.png

outerLogMake挂在了Phone的原型上,因此实例可以顺着原型链调用该方法。

在构造函数内部的innerLogMake,事实上它被认为是实例的一个属性,而非方法。为了性能考虑,方法应该挂在Phone.prototype上,而不是每次在执行构造函数时重新生成一个方法。

箭头函数在构造函数中和原型上的差异

function Phone(make, model) {
  this.make = make;
  this.model = model;

  this.innerLogMake_arrow = () => {
    console.log('当前手机的厂商:', this.make);
  }
}

Phone.prototype.outerLogMake_arrow = () => {
      // 这里的 this 不指向 Phone 实例!!!
    console.log('当前手机的厂商:', this.make);
}

const phone = new Phone('Apple', 'iPhone');

phone.innerLogMake_arrow(); // '当前手机的厂商: Apple'
phone.outerLogMake_arrow(); // '当前手机的厂商: undefined'
 

改变实例的原型

实例与原型的连接是通过实例的__proto__表现的。根据上面提到的,如果我们需要改变实例的原型,应该调用Object.setPrototypeOf(o, proto),而不是直接设置__proto__

Object.setPrototypeOf(phone, null);
typeof phone.outerLogMake; // undefined
Object.setPrototypeOf(phone, Phone.prototype);
phone.outerLogMake(); // '当前手机的厂商: Apple'
 

实现继承:

function Phone(make, model) {
  this.make = make;
  this.model = model;
}
Phone.prototype.logMake = function() {
    console.log('当前手机的厂商:', this.make);
}

function HuaweiPhone(model) {
  // 父类的构造函数必须执行一次!
  Phone.call(this, '华为', model); // *
}
Object.setPrototypeOf(HuaweiPhone.prototype, Phone.prototype); // *

const p40 = new HuaweiPhone('P40');

p40.logMake(); // '当前手机的厂商: 华为'
 

打印p40

image_10.png

默认情况下,HuaweiPhone.prototype.__proto__Object.prototype,两个关键步骤实现对Phone.prototype的继承:

  1. 在构造函数HuaweiPhone()(子类)中执行Phone()(父类),无论是用call()apply()还是其他方式,只要能实现Phone类型的实例的属性正确设置就行
  2. HuaweiPhone.prototype的原型设置为Phone.prototype,这样就能调用Phone原型上的属性、方法了

Class

首先明确的是,JavaScript中的Class只是一个语法糖。

语法糖:指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。 语法糖让程序更加简洁,有更高的可读性。——维基百科

JavaScript中的Class本质上还是构造函数。下面用Class、构造函数两种方式声明Phone类型的数据对象:

// class 声明式
class Phone {
  constructor(make, model) {
    this.make = make;
    this.model = model;
  }
  
   logMake() {
     console.log('当前手机的厂商:', this.make);
   }
} 
 
// 构造函数声明式
function Phone(make, model) {
   this.make = make;
   this.model = model;
}
Phone.prototype.logMake = function() {
  console.log('当前手机的厂商:', this.make);
}
 

先出结论:它们是一个东西。

让我们执行实例化代码看一下:

class声明了对象类型,执行实例化:

image_11.png

可以清楚的看到Phone.prototype.constructor后面虽然是class Phone,但其实就是一个函数,argumentscallerlengthname这些函数会有的属性它都有,最重要的是:

Phone.prototype.constructor.__proto__ === Function.prototype; // true 
 

假class,真function,没跑了!

再来看下构造函数声明了对象类型,执行实例化:

image_12.png

可以看到,两个实例化输出的内容几乎没有差别。

class中的继承

class Phone {
  constructor(make, model) {
    this.make = make;
    this.model = model;
  }
  
   logMake() {
     console.log('当前手机的厂商:', this.make);
   }
}

// 使用关键字 extends 实现继承
class HuaweiPhone extends Phone {
  constructor(model) {
    // super 表示执行 Phone 中的 constructor(),必须调用!
    super('华为', model)
  }
} 
 

执行实例化:

image_13.png


以上就是本篇文章全部内容了,如有错误欢迎指正!有不懂的地方欢迎留言评论!

参考资料

  • stackoverflow: how is almost everything in javascript an object

  • MDN: new 运算符

  • MDN: Object.prototype.__proto__

(0)
上一篇 2021年6月4日 下午12:52
下一篇 2021年6月4日 下午1:08

相关推荐

发表回复

登录后才能评论