文章目录
JavaScript 教程
基本语法
1. 语句
var声明变量
var a = 1 + 3;
ECMAScript2015的最好的特性之一是使用const和let声明变量。这些声明是块级作用域(与旧的函数作用域的var相反),并且在声明语句前,变量都处于临时死区,这是一个很大的进步。
2.2 变量提升
console.log(a);
var a = 1;
//不会报错,但是也不会显示1。实际上运行下面的代码
var a;
console.log(a);
a = 1;
3 标识符
第二个字符可以用数字,第一个不可以。下划线可以用在第一个。中文是合法字符。
5 区块
{var a = 1;
a = 2}
console.log(a)
输出:
2
JavaScript 使用大括号,将多个相关的语句组合在一起,称为“区块”(block)。
对于var命令来说,JavaScript 的区块不构成单独的作用域(scope)。
6.1 严格相等运算符(===
)和相等运算符(==
)
简单说,它们的区别是相等运算符(
==
)比较两个值是否相等,严格相等运算符(===
)比较它们是否为“同一个值”。
如果两个值不是同一类型,严格相等运算符(===
)直接返回false,而相等运算符(==
)会将它们转换成同一个类型,再用严格相等运算符进行比较。
6.4 三元运算符
语句如下:
(条件) ? 表达式1 : 表达式2
条件为true,则执行表达式1;条件为false,执行表达式2
var even = (n % 2 === 0) ? true : false;
//等效于
var even;
if (n % 2 === 0) {
even = true;
} else {
even = false;
}
//可用于:
var myVar;
console.log(
myVar ?
'myVar has a value' :
'myVar does not have a value'
)
7.5 标签(label)
JavaScript 语言允许,语句的前面有标签(label),相当于定位符,用于跳转到程序的任意位置,标签的格式如下。
标签可以是任意的标识符,但不能是保留字,语句部分可以是任意语句。
标签通常与break
语句和continue
语句配合使用,跳出特定的循环。
下面代码为一个双重循环区块,break命令后面加上了top标签(注意,top不用加引号),满足条件时,直接跳出双层循环。如果break语句后面不使用标签,则只能跳出内层循环,进入下一次的外层循环。
这个是和java一样的,只是我没怎么用。
top:
for (var i = 0; i < 3; i++){
for (var j = 0; j < 3; j++){
if (i === 1 && j === 1) break top;
console.log('i=' + i + ', j=' + j);
}
}
// i=0, j=0
// i=0, j=1
// i=0, j=2
// i=1, j=0
foo: {
console.log(1);
break foo;
console.log('本行不会输出');
}
console.log(2);
//下面代码中,continue命令后面有一个标签名,
//满足条件时,会跳过当前循环,直接进入下一轮外层循环。
//如果continue语句后面不使用标签,则只能进入下一轮的内层循环。
top:
for (var i = 0; i < 3; i++){
for (var j = 0; j < 3; j++){
if (i === 1 && j === 1) continue top;
console.log('i=' + i + ', j=' + j);
}
}
数据类型
简介
数值(number):整数和小数(比如1和3.14)
字符串(string):文本(比如Hello World)。
布尔值(boolean):表示真伪的两个特殊值,即true(真)和false(假)
undefined:表示“未定义”或不存在,即由于目前没有定义,所以此处暂时没有任何值
null:表示空值,即此处的值为空。
对象(object):各种值组成的集合。
undefined
是什么意思?
undefined和null同样代表空值。它们之间是否有明确的不同?
undefined文档
对象object
是最复杂的数据类型,又可以分成三个子类型:
狭义的对象(object
);数组(array
);函数(function
)
狭义的对象和数组是两种不同的数据组合方式,除非特别声明,本教程的“对象”都特指狭义的对象。函数其实是处理数据的方法,JavaScript 把它当成一种数据类型,可以赋值给变量,这为编程带来了很大的灵活性,也为 JavaScript 的“函数式编程”奠定了基础。
typeof 运算符(数组和对象,null部分感觉很混乱)
JavaScript 有三种方法,可以确定一个值到底是什么类型。
- typeof运算符
- instanceof运算符
- Object.prototype.toString方法
先了解typeof
语法
typeof 123 // "number"
typeof '123' // "string"
typeof false // "boolean"
function f() {}
typeof f
// "function"
//typeof可以用来检查一个没有声明的变量,而不报错。
v
typeof v
// "undefined"
变量v没有用var命令声明,直接使用就会报错。但是,放在typeof后面,就不报错了,而是返回undefined。
// 错误的写法
if (v) {
// ...
}
// ReferenceError: v is not defined
// 正确的写法
if (typeof v === "undefined") {
// ...
}
数组,对象
空数组([])的类型也是object,这表示在 JavaScript 内部,数组本质上只是一种特殊的对象。
typeof window // "object"
typeof {} // "object"
typeof [] // "object"
var o = {};
var a = [];
o instanceof Array // false
a instanceof Array // true
null
typeof null // "object"
null, undefined 和布尔值
布尔值
如果 JavaScript 预期某个位置应该是布尔值,会将该位置上现有的值自动转为布尔值。转换规则是除了下面六个值被转为false,其他值都视为true。
undefined
;null
;false
;0
;NaN
;""
或''
(空字符串)
注意,空数组([]
)和空对象({}
)对应的布尔值,都是true
。
数值
JavaScript 内部,所有数字都是以64位浮点数形式储存,即使整数也是如此。所以,1与1.0是相同的,是同一个数。
1 === 1.0 // true
由于浮点数不是精确的值,所以涉及小数的比较和运算要特别小心。
0.1 + 0.2 === 0.3
// false
0.3 / 0.1
// 2.9999999999999996
(0.3 - 0.2) === (0.2 - 0.1)
// false
parseInt
方法用于将字符串转为整数。
如果字符串头部有空格,空格会被自动去除。
如果parseInt
的参数不是字符串,则会先转为字符串再转换。
字符串转为整数的时候,是一个个字符依次转换,如果遇到不能转为数字的字符,就不再进行下去,返回已经转好的部分。
parseInt(' 81') // 81
//如果`parseInt`的参数不是字符串,则会先转为字符串再转换。
parseInt(1.23) // 1
// 等同于
parseInt('1.23') // 1
//字符串转为整数的时候,是一个个字符依次转换,如果遇到不能转为数字的字符,就不再进行下去,返回已经转好的部分。
parseInt('8a') // 8
parseInt('12**') // 12
parseInt('12.34') // 12
parseInt('15e2') // 15
parseInt('15px') // 15
isFinite
方法返回一个布尔值,表示某个值是否为正常的数值。
isFinite(Infinity) // false
isFinite(-Infinity) // false
isFinite(NaN) // false
isFinite(undefined) // false
isFinite(null) // true
isFinite(-1) // true
字符串
对象
什么是对象?简单说,对象就是一组“键值对”(key-value)的集合,是一种无序的复合数据集合。
var obj = {
foo: 'Hello',
bar: 'World'
};
对象的每一个键名又称为“属性”(property),它的“键值”可以是任何数据类型。如果一个属性的值为函数,通常把这个属性称为“方法”,它可以像函数那样调用。
var obj = {
p: function (x) {
return 2 * x;
}
};
obj.p(1) // 2
如果属性的值还是一个对象,就形成了链式引用。
var o1 = {};
var o2 = { bar: 'hello' };
o1.foo = o2;
o1.foo.bar // "hello"
属性可以动态创建,不必在对象声明时就指定。
var obj = {};
obj.foo = 123;
obj.foo // 123
如果不同的变量名指向同一个对象,那么它们都是这个对象的引用,也就是说指向同一个内存地址。修改其中一个变量,会影响到其他所有变量。
var o1 = {};
var o2 = o1;
o1.a = 1;
o2.a // 1
o2.b = 2;
o1.b // 2
表达式还是语句?
{ foo: 123 }
读取对象的属性,有两种方法,一种是使用点运算符,还有一种是使用方括号运算符。
请注意,如果使用方括号运算符,键名必须放在引号里面,否则会被当作变量处理。
var obj = {
p: 'Hello World'
};
obj.p // "Hello World"
obj['p'] // "Hello World"
//如果没有引号
var foo = 'bar';
var obj = {
foo: 1,
bar: 2
};
obj.foo // 1
obj[foo] // 2
数字键可以不加引号,因为会自动转成字符串。
数值键名不能使用点运算符(因为会被当成小数点),只能使用方括号运算符。
var obj = {
123: 'hello world'
};
obj.123 // 报错
obj[123] // "hello world"
查看一个对象本身的所有属性,可以使用Object.keys方法。
var obj = {
key1: 1,
key2: 2
};
Object.keys(obj);
// ['key1', 'key2']
属性的删除命令
注意,删除一个不存在的属性,delete不报错,而且返回true。
有一种情况,delete命令会返回false,那就是该属性存在,且不得删除。
var obj = { p: 1 };
Object.keys(obj) // ["p"]
delete obj.p // true
obj.p // undefined
Object.keys(obj) // []
var obj = {};
delete obj.p // true
属性是否存在:in 运算符。可以是继承的属性。
如果需要看是否是自己的属性,可以用hasOwnProperty
方法判断一下,是否为对象自身的属性。
var obj = { p: 1 };
'p' in obj // true
'toString' in obj // true
var obj = {};
if ('toString' in obj) {
console.log(obj.hasOwnProperty('toString')) // false
}
for…in循环用来遍历一个对象的全部属性。
- 它遍历的是对象所有可遍历(enumerable)的属性,会跳过不可遍历的属性。
(关于对象属性的可遍历性,参见《标准库》章节中 Object 一章的介绍。) - 它不仅遍历对象自身的属性,还遍历继承的属性。
如果继承的属性是可遍历的,那么就会被for…in循环遍历到。但是,一般情况下,都是只想遍历对象自身的属性,所以使用for…in的时候,应该结合使用hasOwnProperty方法,在循环内部判断一下,某个属性是否为对象自身的属性。
var obj = {a: 1, b: 2, c: 3};
for (var i in obj) {
console.log('键名:', i);
console.log('键值:', obj[i]);
}
var obj = {};
// toString 属性是存在的
obj.toString // toString() { [native code] }
for (var p in obj) {
console.log(p);
} // 没有任何输出
var person = { name: '老张' };
for (var key in person) {
if (person.hasOwnProperty(key)) {
console.log(key);
}
}
// name
with 语句,它的作用是操作同一个对象的多个属性时,提供一些书写的方便。
var obj = {
p1: 1,
p2: 2,
};
with (obj) {
p1 = 4;
p2 = 5;
}
// 等同于
obj.p1 = 4;
obj.p2 = 5;
// 例二
with (document.links[0]){
console.log(href);
console.log(title);
console.log(style);
}
// 等同于
console.log(document.links[0].href);
console.log(document.links[0].title);
console.log(document.links[0].style);
注意,如果with区块内部有变量的赋值操作,必须是当前对象已经存在的属性,否则会创造一个当前作用域的全局变量。
函数
函数是一段可以反复调用的代码块。函数还能接受输入的参数,不同的参数会返回不同的值。
三种函数的声明方式
var v = 1;
function f(){
var v = 2;
console.log(v);
}
f() // 2
v // 1
函数参数不是必需的,JavaScript 允许省略参数。
function f(a, b) {
return a;
}
f(1, 2, 3) // 1
f(1) // 1
f() // undefined
f.length // 2
但是,没有办法只省略靠前的参数,而保留靠后的参数。如果一定要省略靠前的参数,只有显式传入undefined
。
函数参数如果是原始类型的值(数值、字符串、布尔值),传递方式是传值传递(passes by value)。这意味着,在函数体内修改参数值,不会影响到函数外部。
但是,如果函数参数是复合类型的值(数组、对象、其他函数),传递方式是传址传递(pass by reference)。也就是说,传入函数的原始值的地址,因此在函数内部修改参数,将会影响到原始值。.注意,如果函数内部修改的,不是参数对象的某个属性,而是替换掉整个参数,这时不会影响到原始值。
var obj = { p: 1 };
function f(o) {
o.p = 2;
}
f(obj);
obj.p // 2
var obj = [1, 2, 3];
function f(o) {
o = [2, 3, 4];
}
f(obj);
obj // [1, 2, 3]
闭包
function f1() {
var n = 999;
function f2() {
console.log(n);
}
return f2;
}
var result = f1();
result(); // 999
数组
length属性是可写的。如果人为设置一个小于当前成员个数的值,该数组的成员会自动减少到length设置的值。
清空数组的一个有效方法,就是将length属性设为0。
如果人为设置length大于当前元素个数,则数组的成员数量会增加到这个值,新增的位置都是空位。
上面代码表示,当length属性设为大于数组个数时,读取新增的位置都会返回undefined。
var arr = [ 'a', 'b', 'c' ];
arr.length // 3
arr.length = 2;
arr // ["a", "b"]
值得注意的是,由于数组本质上是一种对象,所以可以为数组添加属性,但是这不影响length属性的值。
var a = [];
a['p'] = 'abc';
a.length // 0
a[2.1] = 'abc';
a.length // 0
当数组的某个位置是空元素,即两个逗号之间没有任何值,我们称该数组存在空位(hole)。
使用delete命令删除一个数组成员,会形成空位,并且不会影响length属性。
var a = [1, , 1];
a.length // 3
var a = [1, 2, 3];
delete a[1];
a[1] // undefined
a.length // 3
算术运算符
2.2 对象的相加
一般来说,对象的valueOf方法总是返回对象自身,这时再自动调用对象的toString方法,将其转为字符串。
var obj = { p: 1 };
obj + 2 // "[object Object]2"
var obj = { p: 1 };
obj.valueOf() // { p: 1 }
var obj = { p: 1 };
obj.valueOf().toString() // "[object Object]"
知道了这个规则以后,就可以自己定义valueOf方法或toString方法,得到想要的结果。
var obj = {
valueOf: function () {
return 1;
}
};
obj + 2 // 3
这里有一个特例,如果运算子是一个Date对象的实例,那么会优先执行toString方法。
var obj = new Date();
obj.valueOf = function () { return 1 };
obj.toString = function () { return 'hello' };
obj + 2 // "hello2"
3 余数运算符(%
)返回前一个运算子被后一个运算子除,所得的余数。
需要注意的是,运算结果的正负号由第一个运算子的正负号决定。所以,为了得到负数的正确余数值,可以先使用绝对值函数。
余数运算符还可以用于浮点数的运算。但是,由于浮点数不是精确的值,无法得到完全准确的结果。
-1 % 2 // -1
1 % -2 // 1
// 错误的写法
function isOdd(n) {
return n % 2 === 1;
}
isOdd(-5) // false
isOdd(-4) // false
// 正确的写法
function isOdd(n) {
return Math.abs(n % 2) === 1;
}
isOdd(-5) // true
isOdd(-4) // false
6.5 % 2.1
// 0.19999999999999973
4 自增和自减运算符
自增和自减运算符有一个需要注意的地方,就是放在变量之后,会先返回变量操作前的值,再进行自增/自减操作;放在变量之前,会先进行自增/自减操作,再返回变量操作后的值。
var x = 1;
var y = 1;
x++ // 1
++y // 2
5 数值运算符,负数值运算符
数值运算符(+)同样使用加号,但它是一元运算符(只需要一个操作数),而加法运算符是二元运算符(需要两个操作数).数值运算符的作用在于可以将任何值转为数值(与Number函数的作用相同)。
+true // 1
+[] // 0
+{} // NaN
var x = 1;
-x // -1
-(-x) // 1
布尔运算符
3 且运算符(&&)
如果第一个运算子的布尔值为true,则返回第二个运算子的值(注意是值,不是布尔值);如果第一个运算子的布尔值为false,则直接返回第一个运算子的值,且不再对第二个运算子求值。
't' && '' // ""
't' && 'f' // "f"
't' && (1 + 2) // 3
'' && 'f' // ""
'' && '' // ""
var x = 1;
(1 - 1) && ( x += 1) // 0
x // 1
这种跳过第二个运算子的机制,被称为“短路”。有些程序员喜欢用它取代if结构,比如下面是一段if结构的代码,就可以用且运算符改写。
if (i) {
doSomething();
}
// 等价于
i && doSomething();
二进制位运算符
位运算符只对整数起作用,如果一个运算子不是整数,会自动转为整数后再执行。另外,虽然在 JavaScript内部,数值都是以64位浮点数的形式储存,但是做位运算的时候,是以32位带符号的整数进行运算的,并且返回值也是一个32位带符号的整数。
function toInt32(x) {
return x | 0;
toInt32(1.001) // 1
toInt32(1.999) // 1
toInt32(1) // 1
toInt32(-1) // -1
toInt32(Math.pow(2, 32) + 1) // 1
toInt32(Math.pow(2, 32) - 1) // -1
}
二进制或运算符
位运算只对整数有效,遇到小数时,会将小数部分舍去,只保留整数部分。所以,将一个小数与0进行二进制或运算,等同于对该数去除小数部分,即取整数位。
2.9 | 0 // 2
-2.9 | 0 // -2
二进制与运算符。
0 & 3 // 0
二进制否运算符
二进制否运算符(~)将每个二进制位都变为相反值(0变为1,1变为0)。它的返回结果有时比较难理解,因为涉及到计算机内部的数值表示机制。
3的32位整数形式是00000000000000000000000000000011
,二进制否运算以后得到11111111111111111111111111111100
。由于第一位(符号位)是1,所以这个数是一个负数。JavaScript
内部采用补码形式表示负数,即需要将这个数减去1,再取一次反,然后加上负号,才能得到这个负数对应的10进制值。这个数减去1等于11111111111111111111111111111011
,再取一次反得到00000000000000000000000000000100
,再加上负号就是-4。考虑到这样的过程比较麻烦,可以简单记忆成,一个数与自身的取反值相加,等于-1。
对字符串进行二进制否运算,JavaScript 引擎会先调用Number函数,将字符串转为数值。
Number函数将字符串转为数值的规则。参见《数据的类型转换》一章。
~ 3 // -4
5 异或运算符
异或运算(^)在两个二进制位不同时返回1,相同时返回0。
“异或运算”有一个特殊运用,连续对两个数a和b进行三次异或运算,a^=b
; b^=a
; a^=b
;,可以互换它们的值。这意味着,使用“异或运算”可以在不引入临时变量的前提下,互换两个变量的值。
//0(二进制00)与3(二进制11)进行异或运算,它们每一个二进制位都不同,所以得到11(即3)。
0 ^ 3 // 3
var a = 10;
var b = 99;
a ^= b, b ^= a, a ^= b;
a // 99
b // 10
6 左移运算符
左移运算符(<<)表示将一个数的二进制值向左移动指定的位数,尾部补0,即乘以2的指定次方。向左移动的时候,最高位的符号位是一起移动的。
(为什么是乘以2呢,左移一位和2进制里乘以10
一样呀)
如果左移0位,就相当于将该数值转为32位整数,等同于取整,对于正数和负数都有效。
4 << 1
// 8
-4 << 1
// -8
13.5 << 0
// 13
-13.5 << 0
// -13
7 右移运算符
8 头部补零的右移运算符
头部补零的右移运算符(>>>)与右移运算符(>>)只有一个差别,就是一个数的二进制形式向右移动时,头部一律补零,而不考虑符号位。所以,该运算总是得到正值。对于正数,该运算的结果与右移运算符(>>)完全一致,区别主要在于负数。
9 开关作用
假定某个对象有四个开关,每个开关都是一个变量。那么,可以设置一个四位的二进制数,它的每个位对应一个开关。
var FLAG_A = 1; // 0001
var FLAG_B = 2; // 0010
var FLAG_C = 4; // 0100
var FLAG_D = 8; // 1000
var flags = 5; // 二进制的0101
if (flags & FLAG_C) {
// ...
}
// 0101 & 0100 => 0100 => true
其他运算符,运算顺序
1 void 运算符
void运算符的作用是执行一个表达式,然后不返回任何值,或者说返回undefined
var x = 3;
void (x = 5) //undefined
x // 5
2 逗号运算符
逗号运算符用于对两个表达式求值,并返回后一个表达式的值。
3 运算顺序
语法专题
数据类型的转换
JavaScript 是一种动态类型语言,变量没有类型限制,可以随时赋予任意值。
虽然变量的数据类型是不确定的,但是各种运算符对数据类型是有要求的。如果运算符发现,运算子的类型与预期不符,就会自动转换类型。比如,减法运算符预期左右两侧的运算子应该是数值,如果不是,就会自动将它们转为数值。
var x = y ? 1 : 'a';
'4' - '3' // 1
强制转换
强制转换主要指使用Number()
、String()
和Boolean()
三个函数,手动将各种类型的值,分别转换成数字、字符串或者布尔值。
- Number函数
Number
函数将字符串转为数值,要比parseInt
函数严格很多。基本上,只要有一个字符无法转成数值,整个字符串就会被转为NaN。
parseInt('42 cats') // 42
Number('42 cats') // NaN
简单的规则是,Number方法的参数是对象时,将返回NaN,除非是包含单个数值的数组。
之所以会这样,是因为Number背后的转换规则比较复杂。
-
调用对象自身的
valueOf
方法。如果返回原始类型的值,则直接对该值使用Number
函数,不再进行后续步骤。 -
如果
valueOf
方法返回的还是对象,则改为调用对象自身的toString
方法。如果toString
方法返回原始类型的值,则对该值使用Number函数,不再进行后续步骤。 -
如果
toString
方法返回的是对象,就报错。
对象的valueOf 和 toString方法分别会返回什么啊?
默认情况下,对象的valueOf方法返回对象本身,所以一般总是会调用toString方法,而toString方法返回对象的类型字符串(比如[object Object])。所以,会有下面的结果。
Number({a: 1}) // NaN
Number([1, 2, 3]) // NaN
Number([5]) // 5
var obj = {x: 1};
Number(obj) // NaN
// 等同于
if (typeof obj.valueOf() === 'object') {
Number(obj.toString());
} else {
Number(obj.valueOf());
}
- String函数
String
函数可以将任意类型的值转化成字符串
- 原始类型值
数值:转为相应的字符串。
字符串:转换后还是原来的值。
布尔值:true转为字符串"true",false转为字符串"false"。
undefined:转为字符串"undefined"。
null:转为字符串"null"。 - 对象
String
方法的参数如果是对象,返回一个类型字符串;如果是数组,返回该数组的字符串形式。
String
方法背后的转换规则,与Number
方法基本相同,只是互换了valueOf
方法和toString
方法的执行顺序。
1.先调用自身的toString
方法。如果返回原始类型的值,则对该值使用String
函数,不再进行以下步骤。
2.如果toString
方法返回的是对象,再调用原对象的valueOf方法。如果valueOf
方法返回原始类型的值,则对该值使用String
函数,不再进行以下步骤。
3.如果valueOf
方法返回的是对象,就报错。
下面代码先调用对象的toString方法,发现返回的是字符串[object Object],就不再调用valueOf方法了。
String({a: 1})
// "[object Object]"
// 等同于
String({a: 1}.toString())
// "[object Object]"
- Boolean()
Boolean()
函数可以将任意类型的值转为布尔值。
它的转换规则相对简单:除了以下五个值的转换结果为false,其他的值全部为true。
undefined
null
0(包含-0和+0)
NaN
''(空字符串)
- 自动转换
遇到以下三种情况时,JavaScript 会自动转换数据类型,即转换是自动完成的,用户不可见。
123 + 'abc' // "123abc"
if ('abc') {
console.log('hello')
} // "hello"
+ {foo: 'bar'} // NaN
- [1, 2, 3] // NaN
错误处理机制
上面代码中,我们调用Error构造函数,生成一个实例对象err。Error构造函数接受一个参数,表示错误提示,可以从实例的message属性读到这个参数。抛出Error实例对象以后,整个程序就中断在发生错误的地方,不再往下执行。
-
原生错误类型
提供了七种错误类型 -
自定义错误
继承Error
对象
function UserError(message) {
this.message = message || '默认信息';
this.name = 'UserError';
}
UserError.prototype = new Error();
UserError.prototype.constructor = UserError;
编程风格
console 对象与控制台
标准库
Object对象
JavaScript
原生提供的Object
对象的原生方法分为两类,Object
本身的方法与Object
的实例方法
Object
对象本身的方法
所谓“本身的方法”就是直接定义在Object对象的方法。
比如:下面代码中,print方法就是直接定义在Object对象上。
Object.print = function (o) { console.log(o) };
Object
的实例方法- 所谓实例方法就是定义在
Object
原型对象Object.prototype
上的方法。它可以被Object实例直接使用。
关于原型对象object.prototype的详细解释,参见《面向对象编程》章节。这里只要知道,凡是定义在Object.prototype对象上面的属性和方法,将被所有实例对象共享就可以了。
Object.prototype.print = function () {
console.log(this);
};
var obj = new Object();
obj.print() // Object
属性描述对象
JavaScript 提供了一个内部数据结构,用来描述对象的属性,控制它的行为,比如该属性是否可写、可遍历等等。这个内部数据结构称为“属性描述对象”(attributes object)。每个属性都有自己对应的属性描述对象,保存该属性的一些元信息。
{
value: 123,
writable: false,
enumerable: true,
configurable: false,
get: undefined,
set: undefined
}