JavaScript数据类型

/**
 * https://wangdoc.com/javascript/types/index.html
 * JavaScript 语言的每一个值,都属于某一种数据类型。JavaScript 的数据类型,共有六种。ES6 又新增了第七种 Symbol 类型的值:
 * 原始类型:
 *      1.数值(number):整数和小数(比如1和3.14)。
 *      2.字符串(string):文本(比如Hello World)。
 *      3.布尔值(boolean):表示真伪的两个特殊值,即true(真)和false(假)。
 *
 * 两个特殊值:
 *      4.undefined:表示“未定义”或不存在,即由于目前没有定义,所以此处暂时没有任何值。
 *      5.null:表示空值,即此处的值为空。
 *
 * 合成类型:
 *      6.对象(object):各种值组成的集合。
 *
 *      对象:
 *          1.狭义对象(object)
 *          2.数组(array)
 *          3.函数(function)
 *
 */

/**
 * javascript 有三种方法确定一个值的类型:
 * 1. typeof运算符
 * 2. instanceof运算符
 * 3. Object.prototype.toString方法
 */

// typeof 运算符
typeof 123 // "number"
typeof '123' // "string"
typeof false // "boolean"
function f() {}
typeof f    // "function"
typeof undefined   // "undefined"
typeof window // "object"
typeof {} // "object"
typeof [] // "object"   空数组类型也是 object
typeof null // "object"

// 错误的写法
if (v) {
    // ...
  }
  // ReferenceError: v is not defined

  // 正确的写法
  if (typeof v === "undefined") {
    // ...
  }

// 2.2 null 和 undefined
// null是一个表示“空”的对象,转为数值时为0;
// undefined是一个表示"此处无定义"的原始值,转为数值时为NaN
if (!undefined) {
    console.log('undefined is false');
  }
  // undefined is false

  if (!null) {
    console.log('null is false');
  }
  // null is false

  undefined == null
  // true

Number(undefined) // NaN
5 + undefined // NaN

// 变量声明了,但没有赋值
var i;
i // undefined

// 调用函数时,应该提供的参数没有提供,该参数等于 undefined
function f(x) {
  return x;
}
f() // undefined

// 对象没有赋值的属性
var  o = new Object();
o.p // undefined

// 函数没有返回值时,默认返回 undefined
function f() {}
f() // undefined

/**
 * 下列运算符会返回布尔值:
 * 1.前置逻辑运算符: ! (Not)
 * 2.相等运算符:===,!==,==,!=
 * 3.比较运算符:>,>=,<,<=
 *
 * 4.以下值为 false,其余均为 true
 * undefined
 * null
 * false
 * 0
 * NaN
 * ""或''(空字符串)
 */

// 2.3 数值
// 整数和浮点数
// 所有数字都是以64位浮点数形式储存,即使整数也是如此;JavaScript 语言的底层根本没有整数,所有数字都是小数(64位浮点数)
// 某些运算只有整数才能完成,此时 JavaScript 会自动把64位浮点数,转成32位整数,然后再进行运算
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

// 数值精度
// 根据国际标准 IEEE 754,JavaScript 浮点数的64个二进制位,从最左边开始,是这样组成的。
// 1.第1位:符号位,0表示正数,1表示负数
// 2.第2位到第12位(共11位):指数部分
// 3.第13位到第64位(共52位):小数部分(即有效数字)
// -----------------------------------------------
// 0(1)     xxxxxxxxxxx        xxxx....xxxx
// 符号位    指数位,0-2047     小数部分,52位
// -----------------------------------------------
// 如果指数部分的值在0到2047之间(不含两个端点),有效数字的第一位默认总是1,不保存在64位浮点数之中。即 1.xxxxxxx,1不保存在64位浮点中;
//     因此,JavaScript 提供的有效数字最长为53个二进制位。精度最多只能到53个二进制位,
//     这意味着,绝对值小于2的53次方的整数,即-253到253,都可以精确表示。
Math.pow(2, 53)  // 9007199254740992
Math.pow(2, 53) + 1  // 9007199254740992    整数运算结果开始出现错误
Math.pow(2, 53) + 2  // 9007199254740994
Math.pow(2, 53) + 3  // 9007199254740996
Math.pow(2, 53) + 4  // 9007199254740996
Number.MAX_VALUE // 1.7976931348623157e+308
Number.MIN_VALUE // 5e-324

// 科学计数法
//(1)小数点前的数字多于21位。
1234567890123456789012  // 1.2345678901234568e+21
123456789012345678901  // 123456789012345680000
//(2)小数点后的零多于5个。
// 小数点后紧跟5个以上的零,
// 就自动转为科学计数法
0.0000003 // 3e-7
// 否则,就保持原来的字面形式
0.000003 // 0.000003

// 进制
// 默认情况下,JavaScript 内部会自动将八进制、十六进制、二进制转为十进制
// 有前导0的数值会被视为八进制,但是如果前导0后面有数字8和9,则该数值被视为十进制。ES5 的严格模式和 ES6,已经废除了这种表示法
0xzz // 报错
0o88 // 报错
0b22 // 报错
0888 // 888
0777 // 511

// 2.4 特殊数值
// 正零和负零
-0 === +0 // true
0 === -0 // true
0 === +0 // true
+0 // 0
-0 // 0
(-0).toString() // '0'
(+0).toString() // '0'
(1 / +0) === (1 / -0) // false , 除以正零得到+Infinity,除以负零得到-Infinity,这两者是不相等的

// NaN (Not a Number)
0 / 0 // NaN
Math.acos(2) // NaN
Math.log(-1) // NaN
Math.sqrt(-1) // NaN
// NaN不是独立的数据类型,而是一个特殊数值,它的数据类型依然属于Number
typeof NaN // 'number'
NaN === NaN // false  不等于任何值,包括本身,与任何值运算,得到的也是 NaN
[NaN].indexOf(NaN) // -1 数组的indexOf方法内部使用的是严格相等运算符,所以该方法对NaN不成立。
Boolean(NaN) // false

//Infinity
// 表示无穷,两种场景:(1)正的数值太大,或一个负的数值太小;(2)非0数值除以0
// Infinity表示正的无穷,-Infinity表示负的无穷
// Infinity大于一切数值(除了NaN),-Infinity小于一切数值(除了NaN)
// Infinity与NaN比较,总是返回false
// 场景一
Math.pow(2, 1024)  // Infinity
// 场景二
0 / 0 // NaN
1 / 0 // Infinity
Infinity === -Infinity // false
1 / -0 // -Infinity
-1 / -0 // Infinity
Infinity > 1000 // true
-Infinity < -1000 // true
Infinity > NaN // false
-Infinity > NaN // false
Infinity < NaN // false
-Infinity < NaN // false

// 2.5 与数值相关全局方法
// parseInt方法用于将字符串转为整数,头部空格会被自动清除
parseInt('  123') // 123
// 如果parseInt的参数不是字符串,则会先转为字符串再转换 parseInt(String(str),10)
parseInt(1.23) // 1
// 等同于
parseInt('1.23') // 1
// 字符串转为整数的时候,是一个个字符依次转换,如果遇到不能转为数字的字符,就不再进行下去,返回已经转好的部分
parseInt('8a') // 8
parseInt('12**') // 12
parseInt('12.34') // 12
parseInt('15e2') // 15
parseInt('15px') // 15
parseInt('abc') // NaN
parseInt('.3') // NaN
parseInt('') // NaN
parseInt('+') // NaN
parseInt('+1') // 1
// 如果字符串以0x或0X开头,parseInt会将其按照十六进制数解析
parseInt('0x10') // 16
parseInt('011') // 11
// 科学计数法
parseInt(1000000000000000000000.5) // 1
// 等同于
parseInt('1e+21') // 1
parseInt(0.0000008) // 8
// 等同于
parseInt('8e-7') // 8
// 进制转换,默认 默认十进制转十进制
parseInt('1000') // 1000
// 等同于
parseInt('1000', 10) // 1000
parseInt('1000', 2) // 8    二进制转十进制
parseInt('1000', 6) // 216  六进制转十进制
parseInt('1000', 8) // 512  八进制转十进制
// 进制只有在2到36之间,才能得到有意义的结果,超出这个范围,则返回NaN。如果第二个参数(进制)是0、undefined和null,则直接忽略。
parseInt('10', 37) // NaN
parseInt('10', 1) // NaN
parseInt('10', 0) // 10
parseInt('10', null) // 10
parseInt('10', undefined) // 10

// parseFloat方法用于将一个字符串转为浮点数。
parseFloat('3.14') // 3.14
parseFloat('314e-2') // 3.14
parseFloat('0.0314E+2') // 3.14
parseFloat('3.14more non-digit characters') // 3.14 包含不能转为浮点数的字符,则不再进行往后转换,返回已经转好的部分
parseFloat('\t\v\r12.34\n ') // 12.34   自动过滤字符串前导的空格
// 如果参数不是字符串,或者字符串的第一个字符不能转化为浮点数,则返回NaN
parseFloat([]) // NaN
parseFloat('FF2') // NaN
parseFloat('') // NaN
parseFloat(true)  // NaN
Number(true) // 1
parseFloat(null) // NaN
Number(null) // 0
parseFloat('') // NaN
Number('') // 0
parseFloat('123.45#') // 123.45
Number('123.45#') // NaN

// isNaN方法可以用来判断一个值是否为NaN
isNaN(NaN) // true
isNaN(123) // false
isNaN('Hello') // true
// 相当于
isNaN(Number('Hello')) // true
// 对于对象和数组,isNaN也返回true
isNaN({}) // true
// 等同于
isNaN(Number({})) // true
isNaN(['xzy']) // true
// 等同于
isNaN(Number(['xzy'])) // true
//对于空数组和只有一个数值成员的数组,isNaN返回false
isNaN([]) // false
isNaN([123]) // false
isNaN(['123']) // false
// 判断是否 NaN 的方法
function myIsNaN(value) {
    return value !== value;
}

// isFinite方法返回一个布尔值,表示某个值是否为正常的数值
isFinite(Infinity) // false
isFinite(-Infinity) // false
isFinite(NaN) // false
isFinite(undefined) // false
isFinite(null) // true
isFinite(-1) // true

/**
 * 3 字符串
 * 零个或多个排在一起的字符,放在单引号或双引号之中。
 * 单引号字符串的内部,可以使用双引号。双引号字符串的内部,可以使用单引号
 * 由于 HTML 语言的属性值使用双引号,所以很多项目约定 JavaScript 语言的字符串只使用单引号
 */

// 连接运算符(+)可以连接多个单行字符串,将长字符串拆成多行书写,输出的时候也是单行。
var longString = 'Long '
  + 'long '
  + 'long '
  + 'string';

// 利用多行注释,输出多行字符串的方法
(function () { /*
line 1
line 2
line 3
*/}).toString().split('\n').slice(1, -1).join('\n')
// "line 1
// line 2
// line 3"

// 转义符
/*
\0 :null(\u0000)
\b :后退键(\u0008)
\f :换页符(\u000C)
\n :换行符(\u000A)
\r :回车键(\u000D)
\t :制表符(\u0009)
\v :垂直制表符(\u000B)
\' :单引号(\u0027)
\" :双引号(\u0022)
\\ :反斜杠(\u005C)
*/

// 反斜杠还有三种特殊用法。
// 1. 后面跟三个八进制数,\HHH , 代表一个字符,这种方法只能输出256种字符
console.log("\251")     // © 表示版权符
// 2. \x 后面跟两个十六进制数(00到FF),代表一个字符,\xHH, HH对应该字符的 Unicode 码点
console.log("\xA9")     // © 表示版权符
// 3. \u 后面紧跟四个十六进制数(0000到FFFF),代表一个字符。XXXX对应该字符的 Unicode 码点
console.log("\u00A9")   // © 表示版权符
'\251' // "©"
'\xA9' // "©"
'\u00A9' // "©"
'\172' === 'z' // true
'\x7A' === 'z' // true
'\u007A' === 'z' // true

// 字符串与数组
var s = 'hello';
s[0] // "h"
s[1] // "e"
s[4] // "o"
// 直接对字符串使用方括号运算符
'hello'[1] // "e"
// 字符串内部的单个字符无法改变和增删
var s = 'hello';
s.length // 5 返回字符串长度

// 字符集
// JavaScript 使用 Unicode 字符集。JavaScript 引擎内部,所有字符都用 Unicode 表示。
// 每个字符在 JavaScript 内部都是以16位(即2个字节)的 UTF-16 格式储存
// UTF-16 有两种长度:
//  对于码点在U+0000到U+FFFF之间的字符,长度为16位(即2个字节);
//  对于码点在U+10000到U+10FFFF之间的字符,长度为32位(即4个字节),而且前两个字节在0xD800到0xDBFF之间,后两个字节在0xDC00到0xDFFF之间。
// 总结:对于码点在U+10000到U+10FFFF之间的字符,JavaScript 总是认为它们是两个字符(length属性为2)所以,JavaScript 返回的字符串长度可能是不正确的。

// Base64 转码
// 一种编码方法,可以将任意值转成 0~9、A~Z、a-z、+和/这64个字符组成的可打印字符。
// 使用它的主要目的,不是为了加密,而是为了不出现特殊字符,简化程序的处理。
// 使用场景:
//      1.文本里面包含一些不可打印的符号,比如 ASCII 码0到31的符号都无法打印出来,这时可以使用 Base64 编码,将它们转成可以打印的字符
//      2.有时需要以文本格式传递二进制数据,那么也可以使用 Base64 编码
// JavaScript 原生提供两个 Base64 相关的方法,注:这两个方法不适合非 ASCII 码的字符,会报错
//      1.btoa():任意值转为 Base64 编码
//      2.atob():Base64 编码转为原来的值
var string = 'Hello World!';
btoa(string) // "SGVsbG8gV29ybGQh"
atob('SGVsbG8gV29ybGQh') // "Hello World!"
btoa('你好') // 报错
// 非 ASCII 码字符转为 Base64 编码,必须中间插入一个转码环节,再使用这两个方法。
function b64Encode(str) {
    return btoa(encodeURIComponent(str));
}
function b64Decode(str) {
    return decodeURIComponent(atob(str));
}
b64Encode('你好') // "JUU0JUJEJUEwJUU1JUE1JUJE"
b64Decode('JUU0JUJEJUEwJUU1JUE1JUJE') // "你好"

/**
 * 4 对象
 * 对象(object)是 JavaScript 语言的核心,最重要的数据类型
 * 对象就是一组“键值对”(key-value)的集合,是一种无序的复合数据集合。
 *
*/
var obj = {
    foo: 'Hello',
    bar: 'World'
  };

// 对象的所有键名都是字符串,所以加不加引号都可以,键名是数值,会被自动转为字符串
// 键名又称属性,属性的值可以是任何数据类型,如果是函数,则把此属性称为“方法”
{foo: 123}  // 代码块,用大括号
({foo: 123})    // 对象,用圆括号

// 对象属性读取、赋值
// 1. 用点运算符
// 2. 用方括号运算符
var obj = {
  p: 'Hello World'
};
obj.p // "Hello World"
obj['p'] // "Hello World"

var obj = {};
obj.foo = 'Hello';
obj['bar'] = 'World';

// 属性的查看,Object.keys
var obj = {
    key1: 1,
    key2: 2
  };
Object.keys(obj);  // ['key1', 'key2']

// 属性的删除
var obj = { p : 1}  
Object.keys(obj)    // ["p"]
delete obj.p // true
obj.p // undefined
Object.keys(obj) // []

// 属性是否存在
// in运算符的一个问题是,它不能识别哪些属性是对象自身的,哪些属性是继承的。
var obj = { p: 1 };
'p' in obj // true
'toString' in obj // true
var obj = {};
if ('toString' in obj) {
  console.log(obj.hasOwnProperty('toString')) // false  判断是否为对象的自身属性
}

// 属性的遍历
// 它遍历的是对象所有可遍历(enumerable)的属性,会跳过不可遍历的属性。
// 它不仅遍历对象自身的属性,还遍历继承的属性。
var obj = {a: 1, b: 2, c: 3};
for (var i in obj) {
  console.log('键名:', i);
  console.log('键值:', obj[i]);
}
// 键名: a
// 键值: 1
// 键名: b
// 键值: 2
// 键名: c
// 键值: 3
// 遍历对象自身属性
var person = { name: '老张' };
for (var key in person) {
  if (person.hasOwnProperty(key)) {
    console.log(key);
  }
}
// name
// -----------------------------------------------------------------------------
// with 语句
// 操作同一个对象的多个属性时,提供一些书写的方便
//格式:
/**
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 obj = {};
with (obj) {
  p1 = 4;
  p2 = 5;
}
obj.p1 // undefined
p1 // 4
// 建议不要使用with语句,可以考虑用一个临时变量代替with
with(obj1.obj2.obj3) {
  console.log(p1 + p2);
}

// 可以写成
var temp = obj1.obj2.obj3;
console.log(temp.p1 + temp.p2);

// ==============================================================================================

/**
 * 函数
 * 三种声明方法:
 * (1)function 命令
 * (2)函数表达式
 * (3)Function 构造函数
 */
// (1)function 命令
function print(s) {
  console.log(s);
}
// (2)函数表达式
var print = function(s) {
  console.log(s);
};
// 采用函数表达式声明函数时,function命令后面不带有函数名。如果加上函数名,该函数名只在函数体内部有效,在函数体外部无效。
var print = function x(){
  console.log(typeof x);
};
x  // ReferenceError: x is not defined
print()  // function

// 数组遍历
var a = [undefined, undefined, undefined];
a.forEach(function (x, i) {
  console.log(i + '. ' + x);
});
// 0. undefined
// 1. undefined
// 2. undefined
for (var i in a) {
  console.log(i);
}
// 0
// 1
// 2
Object.keys(a)
// ['0', '1', '2']

// 将类数组的对象转换为真正的数组
var arr = Array.prototype.slice.call('abc');
arr instanceof Array  // true
arr   // (3) ['a', 'b', 'c']

// “类似数组的对象”还有一个办法可以使用数组的方法,就是通过call()把数组的方法放到对象上面
// 这种方法比直接使用数组原生的forEach要慢,所以最好还是先将“类似数组的对象”转为真正的数组,然后再直接调用数组的forEach方法
function print(value, index) {
  console.log(index + ' : ' + value);
}
Array.prototype.forEach.call(arrayLike, print);