以下是 Faraz Kelhini 的客座文章。其中一些内容超出了我的舒适区,因此我请 Kyle Simpson 为我进行了技术审查。Kyle 在我们的一次 Office Hours 会议中给出的答案非常有趣。内容如下:1) 本文在技术上是正确的。JavaScript 在传统意义上并没有类,这是大多数人将类塞入 JavaScript 的方式。2) 我们可能需要停止将类塞入 JavaScript 中。JavaScript 有对象,我们可以按照其预期方式使用对象来完成相同类型的事情。Kyle 将其称为 OLOO(与其他对象链接的对象)。这是一个介绍。 我认为学习两者都有价值。
深入理解构造函数对于真正理解 JavaScript 语言至关重要。从技术上讲,JavaScript 没有类,但它有构造函数和原型来为 JavaScript 提供类似的功能。实际上,ES2015 中引入的类声明只是现有基于原型的继承的语法糖,并没有真正为语言添加任何额外功能。
在本教程中,我们将详细探讨构造函数,并了解 JavaScript 如何利用它们来创建对象。
创建和使用构造函数
构造函数就像普通函数,但我们使用new
关键字来调用它们。构造函数有两种类型:内置构造函数,例如Array
和Object
,它们在运行时在执行环境中自动可用;以及自定义构造函数,它们为自己的对象类型定义属性和方法。
当您想要创建多个具有相同属性和方法的相似对象时,构造函数非常有用。按照惯例,构造函数的名称使用大写字母开头,以将其与普通函数区分开来。请考虑以下代码
function Book() {
// unfinished code
}
var myBook = new Book();
代码的最后一行创建了一个Book
的实例,并将其赋值给一个变量。虽然Book
构造函数什么也没做,但myBook
仍然是它的一个实例。如您所见,此函数与普通函数之间没有区别,只是它使用new
关键字调用,并且函数名称以大写字母开头。
确定实例的类型
要找出某个对象是否是另一个对象的实例,我们使用instanceof
运算符
myBook instanceof Book // true
myBook instanceof String // false
请注意,如果instanceof
运算符的右侧不是函数,它将抛出错误。
myBook instanceof {};
// TypeError: invalid 'instanceof' operand ({})
另一种查找实例类型的方法是使用constructor
属性。请考虑以下代码片段
myBook.constructor === Book; // true
myBook
的constructor
属性指向Book
,因此严格相等运算符返回true
。JavaScript 中的每个对象都从其原型继承了一个constructor
属性,该属性指向创建该对象的构造函数。
var s = new String("text");
s.constructor === String; // true
"text".constructor === String; // true
var o = new Object();
o.constructor === Object; // true
var o = {};
o.constructor === Object; // true
var a = new Array();
a.constructor === Array; // true
[].constructor === Array; // true
但是请注意,使用constructor
属性检查实例类型通常被认为是不好的做法,因为它可能会被覆盖。
自定义构造函数
构造函数就像一个饼干切割器,用于创建具有相同属性和方法的多个对象。请考虑以下示例
function Book(name, year) {
this.name = name;
this.year = '(' + year + ')';
}
Book
构造函数需要两个参数:name
和year
。当使用new
关键字调用构造函数时,它会将接收到的参数分配给当前实例的name
和year
属性,如下所示
var firstBook = new Book("Pro AngularJS", 2014);
var secondBook = new Book("Secrets Of The JavaScript Ninja", 2013);
var thirdBook = new Book("JavaScript Patterns", 2010);
console.log(firstBook.name, firstBook.year);
console.log(secondBook.name, secondBook.year);
console.log(thirdBook.name, thirdBook.year);
此代码将以下内容记录到控制台

如您所见,我们可以通过使用不同的参数调用Book
构造函数来快速构建大量不同的书籍对象。这与 JavaScript 在其内置构造函数(如Array()
和Date()
)中使用的模式完全相同。
Object.defineProperty() 方法
Object.defineProperty()
方法可以在构造函数内部使用,以帮助执行所有必要的属性设置。请考虑以下构造函数
function Book(name) {
Object.defineProperty(this, "name", {
get: function() {
return "Book: " + name;
},
set: function(newName) {
name = newName;
},
configurable: false
});
}
var myBook = new Book("Single Page Web Applications");
console.log(myBook.name); // Book: Single Page Web Applications
// we cannot delete the name property because "configurable" is set to false
delete myBook.name;
console.log(myBook.name); // Book: Single Page Web Applications
// but we can change the value of the name property
myBook.name = "Testable JavaScript";
console.log(myBook.name); // Book: Testable JavaScript
此代码使用Object.defineProperty()
来定义访问器属性。访问器属性不包含任何属性或方法,但它们定义了一个在读取属性时调用的 getter 和一个在写入属性时调用的 setter。
getter 预期返回一个值,而 setter 接收作为参数分配给属性的值。上面的构造函数返回一个实例,其name
属性可以设置或更改,但不能删除。当我们获取name
的值时,getter 会在名称前加上字符串Book:
并将其返回。
对象字面量表示法优于构造函数
JavaScript 语言有九个内置构造函数:Object()
、Array()
、String()
、Number()
、Boolean()
、Date()
、Function()
、Error()
和RegExp()
。在创建值时,我们可以自由地使用对象字面量或构造函数。但是,对象字面量不仅更易于阅读,而且运行速度更快,因为它们可以在解析时进行优化。因此,对于简单对象,最好坚持使用字面量。
// a number object
// numbers have a toFixed() method
var obj = new Object(5);
obj.toFixed(2); // 5.00
// we can achieve the same result using literals
var num = 5;
num.toFixed(2); // 5.00
// a string object
// strings have a slice() method
var obj = new String("text");
obj.slice(0,2); // "te"
// same as above
var string = "text";
string.slice(0,2); // "te"
如您所见,对象字面量和构造函数之间几乎没有区别。更有趣的是,仍然可以在字面量上调用方法。当在字面量上调用方法时,JavaScript 会自动将字面量转换为临时对象,以便方法可以执行操作。一旦不再需要临时对象,JavaScript 就会将其丢弃。
使用 new 关键字至关重要
务必记住,在所有构造函数之前使用new
关键字。如果您不小心忘记了new
,您将修改全局对象而不是新创建的对象。请考虑以下示例
function Book(name, year) {
console.log(this);
this.name = name;
this.year = year;
}
var myBook = Book("js book", 2014);
console.log(myBook instanceof Book);
console.log(window.name, window.year);
var myBook = new Book("js book", 2014);
console.log(myBook instanceof Book);
console.log(myBook.name, myBook.year);
此代码将以下内容记录到控制台

当我们不使用new
调用Book
构造函数时,实际上是在调用没有返回语句的函数。结果,构造函数内部的this
指向Window
(而不是myBook
),并且创建了两个全局变量。但是,当我们使用new
调用该函数时,上下文将从全局(Window)切换到实例。因此,this
正确地指向myBook
。
请注意,在严格模式下,此代码将抛出错误,因为严格模式旨在保护程序员免于意外地不使用new
关键字调用构造函数。
作用域安全的构造函数
如我们所见,构造函数只是一个函数,因此可以不使用new
关键字来调用它。但是,对于经验不足的程序员来说,这可能是错误的来源。作用域安全的构造函数旨在无论是否使用new
调用都返回相同的结果,因此它不会遇到这些问题。
大多数内置构造函数,例如Object
、Regex
和Array
,都是作用域安全的。它们使用特殊模式来确定构造函数的调用方式。如果不使用new
,它们会通过使用new
再次调用构造函数来返回对象的正确实例。请考虑以下代码
function Fn(argument) {
// if "this" is not an instance of the constructor
// it means it was called without new
if (!(this instanceof Fn)) {
// call the constructor again with new
return new Fn(argument);
}
}
因此,我们构造函数的作用域安全版本如下所示
function Book(name, year) {
if (!(this instanceof Book)) {
return new Book(name, year);
}
this.name = name;
this.year = year;
}
var person1 = new Book("js book", 2014);
var person2 = Book("js book", 2014);
console.log(person1 instanceof Book); // true
console.log(person2 instanceof Book); // true
结论
需要理解的是,ES2015 中引入的类声明仅仅是现有基于原型的继承的语法糖,并没有为 JavaScript 添加任何新的内容。构造函数和原型是 JavaScript 定义相似和相关对象的主要方式。
在本文中,我们深入了解了 JavaScript 构造函数的工作原理。我们了解到,构造函数就像普通的函数,但它们与 new
关键字一起使用。我们看到了构造函数如何使我们能够快速创建具有相同属性和方法的多个相似对象,以及为什么 instanceof
运算符是确定实例类型的最安全方法。最后,我们研究了作用域安全的构造函数,它们可以带或不带 new
关键字调用。
绝对正确!原型设计模式对我来说一直不太好理解。这个我可以用!
class
一直是保留字,ECMAScript 2015 利用它为 JavaScript 提供了一种经典继承的形式。浏览器尚不支持
class
关键字,但如果使用ESNextBabel,你可以使用ES6ES2015 特性并编译成 ES5 兼容的 JavaScript。class
关键字是否真的是一个“好东西”是目前讨论的热点。我还没有开始使用 ES2015 特性,所以还无法做出判断。这篇文章应该包含关于 ES6 的一部分。
清晰简洁的解释。我想补充一点,如果你想在一个循环中调用多个新的构造函数(可能来自其他项目的列表),这里有一个使用 IIFE 的简单方法
在你的示例中,你不需要 IIFE。这样也能正常工作
也许你想展示其他内容?
你可以完全摆脱糟糕的 for 循环,更多地拥抱 js 的函数式特性。
// 我们简单的构造函数
function MyConstructor(name, order) {
this.name = name;
this.order = order;
}
// 从数组中初始化所有新的构造函数
var myConstructorArray = function(yourArrayOfWhatever) {
return yourArrayOfWhatever.map(function(i, value){
return new MyConstructor(‘item-‘ + (i), i);
});
};
console.log(myConstructorArray([1, 2, 3, 4, 5]));
@hkhjkhj:确实,
map
版本肯定更简洁。(请注意,它是向前循环的,而原始示例是向后循环的。)@Agop 要反转 map 变换,你可以简单地反转数组,然后调用 map。例如
想知道如何在继承/扩展对象时使用它?
在继承的情况下,你可能最好使用 object.create()。对于扩展对象,你可以向原型添加共享属性。
这是我希望这篇文章能涉及到的一个方面(以及原型链)。
基本上,你会像这样进行经典的 JS 继承
这并不是世界上最漂亮的东西,但它在 JS 的原型继承模型中很有意义。
感谢大家的建议,我会尝试一下。
我很失望这篇文章没有涉及 ES6 类
我敢肯定你可以在 Google 上找到很多信息。这是一个 MDN 页面:https://mdn.org.cn/en-US/docs/Web/JavaScript/Reference/Classes
感谢这篇文章。我注意到一个小错误,以下句子“Book 构造函数期望两个参数:name 和 age;”是否应该在结尾处写“year”而不是“age”?
捕捉到了,我已经在文章中修复了。
我不喜欢这种创建对象的方式——使用构造函数和“new”。它充满了问题。
工厂模式是一个更好的替代方案,因为任何函数都可以创建和返回一个对象,并且不需要使用“new”调用它。
Eric Elliot 更详细地解释了为什么工厂优于“new”构造函数 =>
http://ericleads.com/2013/01/javascript-constructor-functions-vs-factory-functions/
至于 ES6 类——它是该语言的一个糟糕的补充。我的观点是,如果你想使用类,就不要写 JavaScript。
我还认为继承被过度使用了,在现代计算中,只有当你创建数万个对象时才真正需要它。
函数组合几乎总是比继承更有利。Spotify 的高级开发人员 Mattias Johansson 在这段视频中雄辩地解释了原因:=> https://www.youtube.com/watch?v=wfMtDGfHWpA
你应该区分构造函数的属性和构造函数的函数。
我决定开始学习 JavaScript。一开始我感到困惑,因为我习惯了通俗易懂的术语。感谢你的帖子,它给了我需要的信息和清晰度。谢谢!