通过本篇文章,你可以学习|复习下面的知识点:
-
new
运算符的使用 -
数据类型、原始值
-
内置的构造函数、自定义构造函数
-
原型、原型链
-
ES6 Class
new 运算符
new
用于创建具有构造函数的对象类型的实例。语法如下:
new Constructor[([args])];
复制代码
其中,Constructor
即构造函数,构造函数的参数args
是可选的;当没有args
时,new Constructor
和new Constructor()
是等同的。
执行 new
时,大致做了这些事情:
-
创建一个空对象
{}
-
将空对象的
__proto__
指向Constructor.prototype
(实现类型的继承,构建原型链) -
将创建的对象作为构造函数中的
this
,执行构造函数 -
构造函数没有
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
获取到。(后面借助这一点,实现获取任意对象的类型)
数据类型对应的构造函数
八大数据类型中,除了null
和undefined
,其余六种数据类型都有相应的构造函数的,分别是:
-
Object()
-
Boolean()
-
Number()
-
String()
-
Symbol()
-
BigInt()
后两个Symbol()
和BigInt()
是在ES6中新增的基础类型,它们无法被new
运算,只能通过调用构造函数的方式,获得一个原始值,如:const typeSymbol = Symbol('type');
。
之所以无法被new
运算,是因为在ES6中规定:不能对基础类型的构造函数执行new
运算。而Boolean()
、Number()
和String()
还支持new
运算,更多是为了兼容性考虑。现在推荐使用字面量方式创建原始值。
如果真的想获得一个原始值包装器(含有[[PrimitiveValue]]
的对象),可以使用构造函数Object()
,调用valueOf()
方法即可获取到原始值:
new Object()
和Object()
的效果几乎是一样的。
在JavaScript中,当我们说到“对象”或“object”时,通常指的是包含键值对的实例;当我们说到“Object”时,通常指的是构造函数Object()
。
原始值包装器
以Boolean
为例,执行new Boolean(0)
:
控制台中打印的Boolean {false}
就是一个原始值包装器,它有两个特点:
- 它是一个继承自
Boolean.prototype
的对象(该对象继承自Object.prototype
),因此若执行new Boolean(false) && 100
实际上返回的是100
- 它包裹了一个值为
false
的有原始值(PrimitiveValue)。
如果不使用运算符new
只执行构造函数,将只返回原始值(有需要的话进行数据转换):
基本类型
七个基本类型:
-
Null:
null
-
Undefined:
undefined
-
Boolean:
true
false
-
Number:
1
100
… -
String:
'hello'
'你好'
… -
Symbol:
Symbol.toStringTag
Symbol.toPromitive
… -
BigInt:
1n
9999n
…
基本类型代表了最底层的语言实现。
基本类型的值被称为原始值、原始数据,如null
、undefined
、true
、100
、'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')
控制台输出:
这里我们创建了一个Phone
类型的实例,属性make
和model
都在执行构造函数时正常赋值了。
此外,该实例还有一个属性__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
属性出现在哪些对象上?
答:内置的构造函数和自定义的普通函数。
箭头函数没有prototype
:
实例也没有prototype
:
与之相应的,__proto__
出现在对象实例上。
很多时候,我们发现原型也有__proto__
,这是因为:原型也是某个其他原型的实例。没有多层继承的话,它通常是Object.prototype
。
Number.prototype.__proto__ === Object.prototype; // true
复制代码
原型的两个基本属性
一个“纯净”的Constructor.prototype
有两个属性:
-
constructor
– 指向构造函数Constructor
-
__proto__
– 原型Constructor.prototype
的__proto__
通常指向Object.prototype
constructor
、__proto__
和prototype
的关系如图:
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;
}
});
复制代码
之后,除了null
和undefined
之外的所有基础类型数据、复杂类型数据,都可以通过调用[Symbol.type]
属性获取其类型:
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
:
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
:
默认情况下,HuaweiPhone.prototype.__proto__
为Object.prototype
,两个关键步骤实现对Phone.prototype
的继承:
- 在构造函数
HuaweiPhone()
(子类)中执行Phone()
(父类),无论是用call()
、apply()
还是其他方式,只要能实现Phone类型的实例的属性正确设置就行 - 将
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声明了对象类型,执行实例化:
可以清楚的看到Phone.prototype.constructor
后面虽然是class Phone
,但其实就是一个函数,arguments
、caller
、length
、name
这些函数会有的属性它都有,最重要的是:
Phone.prototype.constructor.__proto__ === Function.prototype; // true
复制代码
假class,真function,没跑了!
再来看下构造函数声明了对象类型,执行实例化:
可以看到,两个实例化输出的内容几乎没有差别。
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)
}
}
复制代码
执行实例化:
以上就是本篇文章全部内容了,如有错误欢迎指正!有不懂的地方欢迎留言评论!