大家在学 JavaScript 面向对象时,往往会有几个纳闷:
1:为什么 JavaScript(直到 ES6)有对象的概念,然而却没有像其余的语言那样,有类的概念呢;
2:为什么在 JavaScript 对象里能够自在增加属性,而其余的语言却不能呢?
甚至,在一些争执中,有人强调:JavaScript 并非 “ 面向对象的语言 ”,而是“ 基于对象的语言 ”。到底是面向对象还是基于对象这两派谁都压服不了谁。
实际上,基于对象和面向对象两个形容词都呈现在了 JavaScript 规范的各个版本当中。咱们能够先看看 JavaScript 规范对基于对象的定义,这个定义的具体内容是:” 语言和宿主的基础设施由对象来提供,并且 JavaScript 程序即是一系列相互通信的对象汇合 ”。
这里的意思基本不是表白弱化的面向对象的意思,反而是表白对象对于语言的重要性。
咱们首要任务就是去了解面向对象和 JavaScript 中的面向对象到底是什么。
什么是面向对象?
在《面向对象分析与设计》这本书中,作者 替咱们做了总结,他认为,从人类的认知角度来说,对象应该是下列事物之一:
一个能够触摸或者能够看见的货色;
- 人的智力能够了解的货色;
- 能够领导思考或口头(进行设想或施加动作)的货色。
- 有了对象的天然定义后,咱们就能够形容编程语言中的对象了。在不同的编程语言中,设计者也利用各种不 + 同的语言个性来形象形容对象,最为胜利的流派是应用 ” 类 ” 的形式来形容对象,这诞生了诸如 C++、
Java 等风行的编程语言。而 JavaScript 早年却抉择了一个更为冷门的形式:原型。这是我在后面说它不合群的起因之一。
JavaScript 推出之时受管理层之命被要求模拟 Java,所以,JavaScript 创始人 Brendan Eich 在 “ 原型运行时 ” 的根底上引入了 new、this 等语言个性,使之 ” 看起来更像 Java”。这也就造就了 JavaScript 这个乖僻的语言。
首先咱们来理解一下 JavaScript 是如何设计对象模型的。
JavaScript 对象的特色
不管咱们应用什么样的编程语言,咱们都先应该去了解对象的本质特征(参考 Grandy Booch《面向对象分析与设计》)。总结来看,对象有如下几个特点。
- 对象具备惟一标识性:即便完全相同的两个对象,也并非同一个对象。
- 对象有状态:对象具备状态,同一对象可能处于不同状态之下。
- 对象具备行为:即对象的状态,可能因为它的行为产生变迁。
- 咱们先来看第一个特色,对象具备惟一标识性。一般而言,各种语言的对象惟一标识性都是用内存地址来体现的,对象具备惟一标识的内存地址,所以具备惟一的标识。
所以咱们都应该晓得,任何不同的 JavaScript 对象其实是互不相等的,咱们能够看上面的代码,o1 和 o2 初看是两个截然不同的对象,然而打印进去的后果却是 false。
var o1 = {a: 1};
var o2 = {a: 1};
console.log(o1 == o2); // false
对于对象的第二个和第三个特色 “ 状态和行为 ”,不同语言会应用不同的术语来形象形容它们,比方 C++ 中称它们为“ 成员变量 ” 和“ 成员函数 ”,Java 中则称它们为 “ 属性 ” 和“ 办法 ”。
在 JavaScript 中,将状态和行为对立形象为 “ 属性 ”, 这是因为思考到 JavaScript 中将函数设计成一种非凡对象所以 JavaScript 中的行为和状态都能用属性来形象。
上面这段代码其实就展现了一般属性和函数作为属性的一个例子,其中 o 是对象,count 是一个属性,而函数 render 也是一个属性,只管写法不太雷同,然而对 JavaScript 来说,count 和 render 就是两个一般属性。
var o = {
conut: 1,
render() {console.log(this.d);
}
};
所以,总结一句话来看,在 JavaScript 中,对象的状态和行为其实都被形象为了属性。
在实现了对象基本特征的根底上, 我认为,JavaScript 中对象独有的特色是:对象具备高度的动态性,这是因为 JavaScript 赋予了使用者在运行时为对象添改状态和行为的能力。
比方,JavaScript 容许运行时向对象增加属性,这就跟绝大多数基于类的、动态的对象设计齐全不同。如果你用过 Java 或者其它别的语言,必定会产生跟我一样的感触。
上面这段代码就展现了运行时如何向一个对象增加属性,一开始我定义了一个对象 o,定义实现之后,再增加它的属性 b,这样操作是齐全没问题的。
var o = {a: 1};
o.b = 2;
console.log(o.a, o.b); //1 2
为了进步形象能力,JavaScript 的属性被设计成比别的语言更加简单的模式,它提供了数据属性和拜访器属性(getter/setter)两类。
- JavaScript 对象的两类属性
- 对 JavaScript 来说,属性并非只是简略的名称和值,JavaScript 用一组特色(attribute)来形容属性(property)。
先来说第一类属性,数据属性。它比拟靠近于其它语言的属性概念。数据属性具备四个特色。
- value:就是属性的值。
- writable:决定属性是否被赋值。
- enumerable:决定 for in 是否枚举该属性。
- configurable:决定该属性是否被删除或者扭转特征值。
在大多数状况下,咱们只关怀数据属性的值即可。第二类属性是拜访器(getter/setter)属性,它也有四个特色。
- getter:函数或 undefined,在取属性值时被调用。
- setter:函数或 undefined,在设置属性值时被调用。
拜访器属性使得属性在读和写时执行代码,它容许使用者在写和读属性时,失去齐全不同的值,它能够视为一种函数的语法糖。
讲到了这里,如果你了解了对象的特色,也就能够了解为什么会有 “JavaScript 不是面向对象 ” 这样的说法了。
这是因为 JavaScript 的对象设计跟目前支流基于类的面向对象差别十分大。可事实上,这样的对象零碎设计尽管特地,JavaScript 语言规范也曾经明确阐明,JavaScript 是一门面向对象的语言。
类型零碎
接下来持续来聊另一个十分重要的概念,同时也是很容易被大家疏忽的内容,那就是 JavaScript 中的 ’ 类型零碎 ’。
对机器语言来说,所有的数据都是一堆二进制代码,CPU 解决这些数据的时候,并没有类型的概念,CPU 所做的仅仅是挪动数据,比方对其进行移位,相加或相乘。
而在高级语言中,咱们都会为操作的数据赋予指定的类型,类型能够确认一个值或者一组值具备特定的意义和目标。所以,类型是高级语言中的概念。
比方在 C/C++ 中,你须要为要解决的每条数据指定类型,这样定义变量:
int count = 100;
char* name = "zwj";
C/C++ 编译器负责将这些数据片段转换为供 CPU 解决的正确命令,通常是二进制的机器代码。
在 JavaScript 中引擎能够依据数据主动推导出类型,因而就不须要间接指定变量的类型。
var counter = 100;
const name = "ZWJ";
通用的类型有数字类型、字符串、Boolean 类型等等,引入了这些类型之后,编译器或者解释器就能够依据类型来限度一些无害的或者没有意义的操作。
比方在 Python 语言中,如果应用字符串和数字相加就会报错,因为 Python 感觉这是没有意义的。而在 JavaScript 中,字符串和数字相加是有意义的,能够应用字符串和数字进行相加的。
再比方,你让一个字符串和一个字符串相乘,这个操作是没有意义的,所有语言简直都会禁止该操作。
每种语言都定义了本人的类型,还定义了如何操作这些类型,另外还定义了这些类型应该如何相互作用,咱们就把这称为 类型零碎。
对于类型零碎
直观地了解,一门语言的类型零碎定义了各种类型之间应该如何互相操作,比方,两种不同类型相加应该如何解决,两种雷同的类型相加又应该如何解决等。还规定了各种不同类型应该如何互相转换,比方字符串类型如何转换为数字类型。
V8 是怎么认为字符串和数字相加是有意义?
接下来咱们就能够来看看 V8 是怎么解决 1+”2″ 的了。之前咱们提到过它并不会报错而是输入字符串 ”12″.
当有两个值相加的时候,比方:
a+b
V8 会严格依据 ECMAScript 标准来执行操作。ECMAScript 是一个语言规范,JavaScript 就是 ECMAScript 的一个实现,比方在 ECMAScript 就定义了怎么执行加法操作,如下所示:
艰深地了解:
如果 Type(lprim) 和 Type(rprim) 中有一个是 String 则:
- 把 ToString(lprim) 的后果赋给左字符串 (lstr);
- 把 ToString(rprim) 的后果赋给右字符串 (rstr);
- 返回左字符串 (lstr) 和右字符串 (rstr) 拼接的字符串。
如果是其余的(对象) V8 会提供了一个 ToPrimitve 办法,其作用是将 a 和 b 转换为原生数据类型,其转换流程如下:
- 先检测该对象中是否存在 valueOf 办法,如果有并返回了原始类型,那么就应用该值进行强制类型转换;
- 如果 valueOf 没有返回原始类型,那么就应用 toString 办法的返回值;
- 如果 vauleOf 和 toString 两个办法都不返回根本类型值,便会触发一个 TypeError 的谬误。
当 V8 执行 1+”2″ 时,因为这是两个原始值相加,原始值相加的时候,如果其中一项是字符串,那么 V8 会默认将另外一个值也转换为字符串,相当于执行了上面的操作:
Number(1).toString() + "2"
这个过程还有另外一个名词叫装箱转换。
对于装箱转换
每一种根本类型 Number、String、Boolean 在对象中都有对应的构造函数,所谓装箱转换,正是把根本类型转换为对应的对象,它是类型转换中一种相当重要的品种。
在看一个例子:
1.toString();
这里会间接报错,起因如下。
数字间接量
起因是 JavaScript 标准中规定的数字间接量能够反对四种写法:十进制数、二进制整数、八进制整数和十六进制整数。
十进制的 Number 能够带小数,小数点前后局部都能够省略,然而不能同时省略,咱们看几个例子:
.01
12.
12.01
这都是非法的数字间接量。这里就有一个问题,也是咱们刚刚提出的报错问题:
1.toString();
这时候 1. toString()会被当作成一个带有小数的数字整体。所以咱们要把点独自成为一个 token(语义单元),就要退出空格,这样写:
1 .toString();
// 或者
(1).toString();
此时就不会报错了。
然而为什么 1 能调用 tostring 办法,1 不是原始值吗?
这个过程就是经验了装箱转换,在遇到 (1).toString() 依据根本类型 Number 这个构造函数转换成一个对象。
围绕拆箱 装箱 转换能够写出很多有意思的代码。
{}+[]
- 以 {} 结尾的会被解析为语句块
- 此时 + 为一元操作符,非字符串拼接符
- []会隐式调用 toString()办法,将 [] 转化为原始值 ”
- +” 被转化为数字 0
- 扩大:如果将其用 () 括起来,即 ({}+[]),此时会显示 ”[object Object]”,因为此时{} 不再被解析为语句块
[]+{}
- []会隐式调用 toString()办法,将 [] 转化为原始值 ”
- {}会隐式调用 toString()办法,将 {} 转化为原始值 ”[object Object]”
- + 为字符串拼接符
[]+[]
- []会隐式调用 toString()办法,将 [] 转化为原始值 ”
{}+{}
- 以 {} 结尾的会被解析为语句块,即第一个 {} 为语句块
- 此时 + 为一元操作符,非字符串拼接符
- 第二个 {} 会隐式调用 toString()办法,将 {} 转化为原始值 ”[object Object]”
- +”[object Object]” 输入 NaN
- 扩大 在 chrome 浏览器中输入 ”object Object”
前几年比拟恶心的面试题。
([][[]]+[])[+!![]]+([]+{})[!+[]+!![]]
问题合成:
左 ([][[]]+[])[+!![]]
拆分
[+!![]]
!![] => true
[+!![]] => 1
拆分
([][[]]+[])
[][0] => undefined
undefined+[] =>”undefined”
输入:“undefined”[1]
右
([]+{})[!+[]+!![]]
([]+{}) => “[object Object]”
拆
[!+[]+!![]]
!![] => true => 1
+[] => 0
!0 => 1
[1+1] => 2
输入: “[object Object]”[2]
最初: “undefined”[1]+”[object Object]”[2] ==> nb