作者:valentinogagliardi
译者:前端小智
来源:github
阿里云最近在做活动,低至 2 折,有兴趣可以看看:
https://promotion.aliyun.com/…
为了保证的可读性,本文采用意译而非直译。
一切皆对象
咱们经常听到 JS 中“一切皆对象”?有没有问想过这是什么意思?其它语言也有“一切皆对象”之说,如 Python
。但是Python
中的对象不仅仅是像 JS 对象这样的存放值和值的容器。Python
中的对象是一个 类。JS 中有类似的东西,但 JS 中的“对象”只是键和值的容器:
var obj = {name: "Tom", age: 34}
实际上,JS 中的对象是一种“哑”类型,但很多其他实体似乎都是从对象派生出来的。甚至是数组,在 JS 中创建一个数组,如下所示:
var arr = [1,2,3,4,5]
然后用 typeof
运算符检查类型,会看到一个令人惊讶的结果:
typeof arr
"object"
看来数组是一种特殊的对象!即使 JS 中的函数也是对象。如果你深入挖掘,还有更多,创建一个函数,该函数就会附加一些方法:
var a = function(){ return false;}
a.toString()
输出:
"function(){ return false;}"
咱们并没有在函数声明 toString
方法,所以在底层一定还有东西。它从何而来?Object
有一个名为 .toString
的方法。似乎咱们的函数具有相同的 Object
方法。
Object.toString()
这时咱们使用浏览器控制台来查看默认被附加的函数和属性,这个谜团就会变得更加复杂:
谁把这些方法放在函数呢。JS 中的函数是一种特殊的对象,这会不会是个暗示?再看看上面的图片:我们的函数中有一个名为 prototype
的奇怪命名属性,这又是什么鬼?
JS 中的 prototype
是一个对象。它就像一个背包,附着在大多数 JS 内置对象上。例如 Object
, Function
, Array
, Date
, Error
,都有一个“prototype
”:
typeof Object.prototype // 'object'
typeof Date.prototype // 'object'
typeof String.prototype // 'object'
typeof Number.prototype // 'object'
typeof Array.prototype // 'object'
typeof Error.prototype // 'object'
注意内置对象有大写字母:
- String
- Number
- Boolean
- Object
- Symbol
- Null
- Undefined
以下除了 Object
是类型之外,其它是 JS 的基本类型。另一方面,内置对象就像 JS 类型的镜像,也用作函数。例如,可以使用 String
作为函数将数字转换为字符串:
String(34)
现在回到“prototype
”。prototype
是所有公共方法和属性的宿主,从祖先派生的“子”对象可以从使用祖先的方法和属性。也就是说,给定一个原始 prototype
,咱们可以创建新的对象,这些对象将使用一个原型作为公共函数的真实源,不 Look see see。
假设有个要求创建一个聊天应用程序,有个人物对象。这个人物可以发送消息,登录时,会收到一个问候。
根据需求咱们很容易定义这个么一 Person
对象:
var Person = {
name: "noname",
age: 0,
greet: function() {console.log(`Hello ${this.name}`);
}
};
你可能会想知道,为什么这里要使用字面量的方式来声明 Person
对象。稍后会详细说明,现在该 Person
为“模型”
。通过这个模型,咱们使用 Object.create()
来创建以为这个模型为基础的对象。
创建和链接对象
JS 中对象似乎以某种方式链接在一起,Object.create()
说明了这一点,此方法从原始对象开始创建新对象,再来创建一个新Person
对象:
var Person = {
name: "noname",
age: 0,
greet: function() {console.log(`Hello ${this.name}`);
}
};
var Tom = Object.create(Person);
现在,Tom 是一个新的对象,但是咱们没有指定任何新的方法或属性,但它仍然可以访问 Person
中的 name
和age
属性。
var Person = {
name: "noname",
age: 0,
greet: function() {console.log(`Hello ${this.name}`);
}
};
var Tom = Object.create(Person);
var tomAge = Tom.age;
var tomName = Tom.name;
console.log(`${tomAge} ${tomName}`);
// Output: 0 noname
现在,可以从一个共同的祖先开始创建新的 person。但奇怪的是,新对象仍然与原始对象保持连接,这不是一个大问题,因为“子”对象可以自定义属性和方法
var Person = {
name: "noname",
age: 0,
greet: function() {console.log(`Hello ${this.name}`);
}
};
var Tom = Object.create(Person);
Tom.age = 34;
Tom.name = "Tom";
var tomAge = Tom.age;
var tomName = Tom.name;
console.log(`${tomAge} ${tomName}`);
// Output: 34 Tom
这种方式被称为“屏蔽”原始属性。还有另一种将属性传递给新对象的方法。Object.create
将另一个对象作为第二个参数,可以在其中为新对象指定键和值:
var Tom = Object.create(Person, {
age: {value: 34},
name: {value: "Tom"}
});
以这种方式配置的属性默认情况下不可写,不可枚举,不可配置。不可写意味着之后无法更改该属性,更改会被忽略:
var Tom = Object.create(Person, {
age: {value: 34},
name: {value: "Tom"}
});
Tom.age = 80;
Tom.name = "evilchange";
var tomAge = Tom.age;
var tomName = Tom.name;
Tom.greet();
console.log(`${tomAge} ${tomName}`);
// Hello Tom
// 34 Tom
不可枚举意味着属性不会在 for...in
循环中显示,例如:
for (const key in Tom) {console.log(key);
}
// Output: greet
但是正如咱们所看到的,由于 JS 引擎沿着原型链向上查找,在“父”对象上找到 greet
属性。最后,不可配置意味着属性既不能修改也不能删除。
Tom.age = 80;
Tom.name = "evilchange";
delete Tom.name;
var tomAge = Tom.age;
var tomName = Tom.name;
console.log(`${tomAge} ${tomName}`);
// 34 Tom
如果要更改属性的行为,只需配 writable
(可写性),configurable
(可配置),enumerable
(可枚举) 属性即可。
var Tom = Object.create(Person, {
age: {
value: 34,
enumerable: true,
writable: true,
configurable: true
},
name: {
value: "Tom",
enumerable: true,
writable: true,
configurable: true
}
});
现在,Tom
也可以通过以下方式访问greet()
:
var Person = {
name: "noname",
age: 0,
greet: function() {console.log(`Hello ${this.name}`);
}
};
var Tom = Object.create(Person);
Tom.age = 34;
Tom.name = "Tom";
var tomAge = Tom.age;
var tomName = Tom.name;
Tom.greet();
console.log(`${tomAge} ${tomName}`);
// Hello Tom
// 34 Tom
暂时不要过于担心“this
”。拉下来会详细介绍。暂且先记住,“this”是对函数执行的某个对象的引用。在咱们的例子中,greet()
在 Tom
的上下文中运行,因此可以访问“this.name
”。
构建 JavaScript 对象
目前为止,只介绍了关于“prototype”的一点知识,还有玩了一会 Object.create()
之外但咱们没有直接使用它。随着时间的推移出现了一个新的模式:构造函数
。使用函数创建新对象听起来很合理,假设你想将Person
对象转换为函数,你可以用以下方式:
function Person(name, age) {var newPerson = {};
newPerson.age = age;
newPerson.name = name;
newPerson.greet = function() {console.log("Hello" + newPerson.name);
};
return newPerson;
}
因此,不需要到处调用 object.create()
,只需将Person
作为函数调用:
var me = Person("Valentino");
构造函数模式有助于封装一系列 JS 对象的创建和配置。在这里, 咱们使用字面量的方式创建对象。这是一种从面向对象语言借用的约定,其中类名开头要大写。
上面的例子有一个严重的问题:每次咱们创建一个新对象时,一遍又一遍地重复创建 greet()
函数。可以使用 Object.create()
,它会在对象之间创建链接,创建次数只有一次。首先,咱们将greet()
方法移到外面的一个对象上。然后,可以使用 Object.create()
将新对象链接到该公共对象:
var personMethods = {greet: function() {console.log("Hello" + this.name);
}
};
function Person(name, age) {
// greet lives outside now
var newPerson = Object.create(personMethods);
newPerson.age = age;
newPerson.name = name;
return newPerson;
}
var me = Person("Valentino");
me.greet();
// Output: "Hello Valentino"
这种方式比刚开始会点,还可以进一步优化就是使用 prototype
,prototype
是一个对象,可以在上面扩展属性,方法等等。
Person.prototype.greet = function() {console.log("Hello" + this.name);
};
移除了 personMethods
。调整Object.create
的参数,否则新对象不会自动链接到共同的祖先:
function Person(name, age) {
// greet lives outside now
var newPerson = Object.create(Person.prototype);
newPerson.age = age;
newPerson.name = name;
return newPerson;
}
Person.prototype.greet = function() {console.log("Hello" + this.name);
};
var me = Person("Valentino");
me.greet();
// Output: "Hello Valentino"
现在公共方法的来源是 Person.prototype
。使用 JS 中的new
运算符,可以消除 Person
中的所有噪声,并且只需要为 this
分配参数。
下面代码:
function Person(name, age) {
// greet lives outside now
var newPerson = Object.create(Person.prototype);
newPerson.age = age;
newPerson.name = name;
return newPerson;
}
改成:
function Person(name, age) {
this.name = name;
this.age = age;
}
完整代码:
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.greet = function() {console.log("Hello" + this.name);
};
var me = new Person("Valentino");
me.greet();
// Output: "Hello Valentino"
注意,使用 new
关键字,被称为“构造函数调用”
,new
干了三件事情
- 创建一个空对象
- 将空对象的
__proto__
指向构造函数的prototype
- 使用空对象作为上下文的调用构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
根据上面描述的,new Person("Valentino")
做了:
- 创建一个空对象:
var obj = {}
- 将空对象的
__proto__
指向构造函数的 prototype:obj.__proto__ = Person().prototype
- 使用空对象作为上下文调用构造函数:
Person.call(obj)
检查原型链
检查 JS 对象之间的原型链接有很多种方法。例如,Object.getPrototypeOf
是一个返回任何给定对象原型的方法。考虑以下代码:
var Person = {
name: "noname",
age: 0,
greet: function() {console.log(`Hello ${this.name}`);
}
};
var Tom = Object.create(Person);
检查 Person
是否是 Tom
的原型:
var tomPrototype = Object.getPrototypeOf(Tom);
console.log(tomPrototype === Person);
// Output: true
当然,如果使用构造函数调用构造对象,Object.getPrototypeOf
也可以工作。但是应该检查原型对象,而不是构造函数本身:
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.greet = function() {console.log("Hello" + this.name);
};
var me = new Person("Valentino");
var mePrototype = Object.getPrototypeOf(me);
console.log(mePrototype === Person.prototype);
// Output: true
除了 Object.getPrototypeOf
之外,还有另一个方法isPrototypeOf
。该方法用于测试一个对象是否存在于另一个对象的原型链上,如下所示,检查 me
是否在 Person.prototype
上:
Person.prototype.isPrototypeOf(me) && console.log('Yes I am!')
instanceof
运算符也可以用于测试构造函数的 prototype
属性是否出现在对象的原型链中的任何位置。老实说,这个名字有点误导,因为 JS 中没有“实例”。在真正的面向对象语言中,实例是从类创建的新对象。请考虑 Python 中的示例。咱们有一个名为 Person
的类,咱们从该类创建一个名为“tom”的新实例:
class Person():
def __init__(self, age, name):
self.age = age;
self.name = name;
def __str__(self):
return f'{self.name}'
tom = Person(34, 'Tom')
注意,在 Python 中没有 new
关键字。现在,咱们可以使用 isinstance
方法检查 tom
是否是 Person
的实例
isinstance(tom, Person)
// Output: True
Tom
也是 Python
中“object
”的一个实例,下面的代码也返回true
:
isinstance(tom, object)
// Output: True
根据 isinstance
文档,“如果对象参数是类参数的实例,或者是它的 (直接、间接或虚拟) 子类的实例,则返回 true
”。咱们在这里讨论的是类。现在让咱们看看instanceof
做了什么。咱们将从 JS 中的 Person
函数开始创建tom
(因为没有真正的类)
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.greet = function() {console.log(`Hello ${this.name}`);
};
var tom = new Person(34, "Tom");
使用 isinstance
方法检查 tom
是否是 Person
和 Object
的实例
if (tom instanceof Object) {console.log("Yes I am!");
}
if (tom instanceof Person) {console.log("Yes I am!");
}
因此,可以得出结论:JS 对象的原型总是连接到直接的“父对象”和 Object.prototype
。没有像Python
或Java
这样的类。JS 是由对象组成,那么什么是原型链呢? 如果你注意的话,咱们提到过几次“原型链”。JS 对象可以访问代码中其他地方定义的方法,这看起来很神奇。再次考虑下面的例子:
var Person = {
name: "noname",
age: 0,
greet: function() {console.log(`Hello ${this.name}`);
}
};
var Tom = Object.create(Person);
Tom.greet();
即使该方法不直接存在于“Tom
”对象上,Tom
也可以访问greet()
。
这是 JS 的一个内在特征,它从另一种称为 Self
的语言中借用了原型系统。当访问 greet()
时,JS 引擎会检查该方法是否可直接在 Tom
上使用。如果不是,搜索将继续向上链接,直到找到该方法。
“链”是 Tom
连接的原型对象的层次结构。在我们的例子中,Tom
是 Person
类型的对象,因此 Tom
的原型连接到 Person.prototype
。而Person.prototype
是Object
类型的对象,因此共享相同的 Object.prototype
原型。如果在 Person.prototype
上没有greet()
,则搜索将继续向上链接,直到到达Object.prototype
。这就是咱们所说的“原型链”。
保护对象不受操纵
大多数情况下,JS 对象“可扩展”是必要的,这样咱们可以向对象添加新属性。但有些情况下,我们希望对象不受进一步操纵。考虑一个简单的对象:
var superImportantObject = {
property1: "some string",
property2: "some other string"
};
默认情况下,每个人都可以向该对象添加新属性
var superImportantObject = {
property1: "some string",
property2: "some other string"
};
superImportantObject.anotherProperty = "Hei!";
console.log(superImportantObject.anotherProperty); // Hei!
Object.preventExtensions()
方法让一个对象变的不可扩展,也就是永远不能再添加新的属性。
var superImportantObject = {
property1: "some string",
property2: "some other string"
};
Object.preventExtensions(superImportantObject);
superImportantObject.anotherProperty = "Hei!";
console.log(superImportantObject.anotherProperty); // undefined
这种技术对于“保护”代码中的关键对象非常方便。JS 中还有许多预先创建的对象,它们都是为扩展而关闭的,从而阻止开发人员在这些对象上添加新属性。这就是“重要”对象的情况,比如 XMLHttpRequest
的响应。浏览器供应商禁止在响应对象上添加新属性
var request = new XMLHttpRequest();
request.open("GET", "https://jsonplaceholder.typicode.com/posts");
request.send();
request.onload = function() {
this.response.arbitraryProp = "我是新添加的属性";
console.log(this.response.arbitraryProp); // undefined
};
这是通过在“response”对象上内部调用 Object.preventExtensions
来完成的。您还可以使用 Object.isExtensible
方法检查对象是否受到保护。如果对象是可扩展的,它将返回true
:
var superImportantObject = {
property1: "some string",
property2: "some other string"
};
Object.isExtensible(superImportantObject) && console.log("我是可扩展的");
如果对象不可扩展的,它将返回false
:
var superImportantObject = {
property1: "some string",
property2: "some other string"
};
Object.preventExtensions(superImportantObject);
Object.isExtensible(superImportantObject) ||
console.log("我是不可扩展的!");
当然,对象的现有属性可以更改甚至删除
var superImportantObject = {
property1: "some string",
property2: "some other string"
};
Object.preventExtensions(superImportantObject);
delete superImportantObject.property1;
superImportantObject.property2 = "yeees";
console.log(superImportantObject); // {property2: 'yeees'}
现在,为了防止这种操作,可以将每个属性定义为不可写和不可配置。为此,有一个方法叫Object.defineProperties
。
var superImportantObject = {};
Object.defineProperties(superImportantObject, {
property1: {
configurable: false,
writable: false,
enumerable: true,
value: "some string"
},
property2: {
configurable: false,
writable: false,
enumerable: true,
value: "some other string"
}
});
或者,更方便的是,可以在原始对象上使用Object.freeze
:
var superImportantObject = {
property1: "some string",
property2: "some other string"
};
Object.freeze(superImportantObject);
Object.freeze
工作方式与 Object.preventExtensions
相同,并且它使所有对象的属性不可写且不可配置。唯一的缺点是“Object.freeze
”仅适用于对象的第一级:嵌套对象不受操作的影响。
class
有大量关于 ES6 类的文章,所以在这里只讨论几点。JS 是一种真正的面向对象语言吗? 看起来是这样的,如果咱们看看这段代码
class Person {constructor(name) {this.name = name;}
greet() {console.log(`Hello ${this.name}`);
}
}
语法与 Python
等其他编程语言中的类非常相似:
class Person:
def __init__(self, name):
self.name = name
def greet(self):
return 'Hello' + self.name
或 PHP
class Person {
public $name;
public function __construct($name){$this->name = $name;}
public function greet(){echo 'Hello' . $this->name;}
}
ES6 中引入了类。但是在这一点上,咱们应该清楚 JS 中没有“真正的
”类。一切都只是一个对象,尽管有关键字class
,“原型系统”仍然存在。新的 JS 版本是向后兼容的,这意味着在现有功能的基础上添加了新功能,这些新功能中的大多数都是遗留代码的语法糖。
总结
JS 中的几乎所有东西都是一个对象。从字面上看。JS 对象是键和值的容器,也可能包含函数。Object
是 JS 中的基本构建块:因此可以从共同的祖先开始创建其他自定义对象。然后咱们可以通过语言的内在特征将对象链接在一起:原型系统。
从公共对象开始,可以创建共享原始“父”的相同属性和方法的其他对象。但是它的工作方式不是通过将方法和属性复制到每个孩子,就像 OOP 语言那样。在 JS 中,每个派生对象都保持与父对象的连接。使用 Object.create
或使用所谓的构造函数创建新的自定义对象。与 new
关键字配对,构造函数类似于模仿传统的 OOP 类。
思考
- 如何创建不可变的 JS 对象?
- 什么是构造函数调用?
- 什么是构造函数?
- “prototype”是什么?
- 可以描述一下
new
在底层下做了哪些事吗?
代码部署后可能存在的 BUG 没法实时知道,事后为了解决这些 BUG,花了大量的时间进行 log 调试,这边顺便给大家推荐一个好用的 BUG 监控工具 Fundebug。
原文:https://github.com/valentinog…
交流
阿里云最近在做活动,低至 2 折,有兴趣可以看看:https://promotion.aliyun.com/…
干货系列文章汇总如下,觉得不错点个 Star,欢迎 加群 互相学习。
https://github.com/qq449245884/xiaozhi
因为篇幅的限制,今天的分享只到这里。如果大家想了解更多的内容的话,可以去扫一扫每篇文章最下面的二维码,然后关注咱们的微信公众号,了解更多的资讯和有价值的内容。
每次整理文章,一般都到 2 点才睡觉,一周 4 次左右,挺苦的,还望支持,给点鼓励