这篇文章将帮助你学习 |检查以下数据点:
-
使用
new
运算符 -
数据类型、原始值
-
内置构建器、自定义构建器
-
原型、原型链
-
ES6 级
新干员
new
用于实例化具有构造函数的对象类型。语法如下:
new Constructor[([args])];
其中,Constructor
为构造函数,构造函数参数args
为可选;当没有args
时,new Constructor
和new Constructor()
是等价的。
执行new
时,它基本上执行以下操作:
-
创建一个空对象
{}
-
点
__proto__
到Constructor.prototype
(实现类型继承,构建原型链) -
在构造函数中使用创建的对象作为
this
,并执行构造函数 -
构造函数返回构造函数内部的
,这是this
,不带new
运算符的结果(也可以在创建阶段主动返回对象,但不建议重写构造函数中的普通对象 - TypeScript 明确要求构造函数的返回类型必须为void
)
因此,如果 new
用于空函数,则返回空对象 {}
:
new (function() {}); // {}
对于构建者来说,执行new Constructor()
和Constructor()
得到的结果通常是不同的:
typeof Date(); // 'string'
typeof new Date(); // 'object'
new
除了使用构造函数之外,您还可以对 ES6 中的类执行操作。但 ES6 类只是语法糖,本质上还是构造函数,所以我们重点关注构造函数。
内置构建器
内置的构造函数有很多,在日常生活中也经常使用:Date()
、
、Map()
、
// 构造函数声明式
function Phone(make, model) {
this.make = make;
this.model = model;
}
Phone.prototype.logMake = function() {
console.log('当前手机的厂商:', this.make);
}
、…
在 JavaScript 中,构造函数通常使用驼峰命名法命名。
我们可以使用构造函数名称作为构造函数产生的实例的类型,例如new Date()
类型是Date,new Error('出错了出错了啊')
类型是Error,类型是Array,♷Object,100
类型是 Number,'nice !'
类型是 String,true
类型是 Boolean...构造函数的名称我们通过 获取。 (稍后将使用它来确定任何 对象的类型)
数据类型对应的构造函数
八种主要数据类型中,除了null
和undefined
外,其他六种数据类型都有对应的构造函数,分别是:❀
-
Object()
-
Boolean()
-
Number()
-
String()
-
Symbol()
-
BigInt()
最后两个Symbol()
和BigInt()
是ES6中新的基本类型。他们不能使用 new
,只能通过调用构造函数来获取原始值,例如: const typeSymbol = Symbol('type');
。
不能使用 new
的原因是 ES6 规定 new
不能使用基类型构造函数执行。并且Boolean()
、Number()
和String()
也支持new
功能,更多是出于兼容性原因。现在建议使用文字来创建原始值。
如果您确实想获取包裹在(包含条目[[PrimitiveValue]]
的对象)中的的原始值,您可以使用构造函数并调用方法来获取原始值:
new Object()
和Object()
的效果几乎相同。
在 JavaScript 中,当我们说“对象”或“对象”时,我们通常指的是包含键值对的实例;当我们说“对象”时,我们通常指的是构造函数Object()
。
原始值包装
例如
运行:
Boolean {false}
是一个原始值包装器,具有两个属性:
- 是一个继承自
Boolean.prototype
的对象(这个对象继承自Object.prototype
),所以如果运行new Boolean(false) && 100
,返回的是 - 它包含值
void
PrimitiveValue 。
如果只执行构造函数而不使用运算符new
,则仅返回原始值(必要时转换数据):
基本类型
七种基本类型:
-
空:
null
-
未指定:
undefined
-
布尔值:
true
false
-
数量:
1
100
… -
字符串:
'hello'
'你好'
… -
符号:
Symbol.toStringTag
… -
BigInt: …
基本类型代表语言实现的最低级别。
基本类型 的值称为原始值,原始数据如 null
、undefined
、true
、、 。
随着我们的日常开发,我们注意到基本类型在代码中被广泛使用,因此在设计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__
,该属性仅用于一件事:指向当前实例的原型(父类),即实例的❙❙❙❙ 。在上面的示例中,它显示 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.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'
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()
,电话,因为实例类型可以是森林的属性,或。右 - 将
HuaweiPhone.prototype
设置为原型Phone.prototype
以调用电话原型属性和方法
班级
首先,很明显JavaScript的Class只是语法糖。
语法糖:指添加到特定计算机语言中的语法。这个语法不影响语言功能,但是更方便程序员使用。 语法糖使程序更加简洁易读。 ——维基百科
JavaScript 类基本上是一个构造函数。下面使用类和构造函数来声明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);
}
首先让我决定:它们是同一件事。
让我们运行实例代码,看看:
类声明对象类型并执行实例化:
可以清楚地看到,虽然Phone.prototype.constructor
跟在class Phone
后面,但它实际上是一个函数。所有功能它都有,最重要的是:
Phone.prototype.constructor.__proto__ === Function.prototype; // true
假类,真功能,没跑!
让我们看一下定义对象类型并执行实例化的构造函数:
可以看到,两个实例的输出内容几乎没有区别。
班级传承
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)
}
}
执行实例化:
以上就是本文全部内容,如有错误还请指正!如果还有不明白的地方,可以留言!
参考文献
stackoverflow:为什么 javascript 中几乎所有东西都是对象
-
MDN:新运营商
-
MDN:Object.prototype.__proto__
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。