JavaScript

86 阅读25分钟

JavaScript

数据类型

  1. javascript共有八种内置数据类型,其中nnsbsub(number, null, string, boolean, symbol, undefined, BigInt)为基本类型,object(Object,Array,Funciton,RegExp,Date)为引用类型

    • 数字 number
    • 空值 null
    • 字符串 string
    • 布尔值 boolean
    • 符号(唯一) symbol ES6新增
    • undefined 未定义
    • BigInt
    • 对象 object
  2. 可以使用typeof运算符查看数据类型

    // typeof返回结果为首字母小写的字符串
    typeof undefined; // 'undefined'
    
    typeof Symbol(); // 'symbol'
    
    typeof {name: 'liu'}; // 'object
    typeof [1, 2, 3]; // 'object'
    
    typeof null; // 'object'
    // 真正校验是否为null的方法
    var a = null;
    (!a && typeof a === 'object'); // true
    
    typeof function a(m,n) {}; // function
    a.length; // 2(函数参数的个数)
    
  3. undefined和undeclared

    var a;
    a; // undefined(未定义)
    b; // ReferenceError: b is not defined(其实是undeclared,未声明)
    
  4. symbol

    // Symbol值都是唯一的,它能作为对象属性的标识符。
    // Symbol 不用 new 直接使用就行
    let smb0 = Symbol('abc')
    let smb1 = Symbol('abc') // 还是新创建的一个
    smb0 === smb1 // false
    
    // Symbol.for 有自己的一个注册池,使用 Symbol.for 时会看看注册池里有没有注册过,如果没有就
    // 会注册一个 Symbol 值,并把这个值放入注册池,如果有,就把注册过的那个 Symbol 值拿出来使用 
    let smb2 = Symbol.for('abc') // 对全局进行注册并保存
    let smb3 = Symbol.for('abc') // 从全局注册中获取
    smb3 === smb2 // true
    
  5. BigInt

    ​ js中最大的数字是2^53 - 1,可以用Number.MAX_SAFE_INTEGER得到最大值,当这个值再大之后就会导致js计算不准确。而BigInt可以展示任意大的整数。

    let max = Number.MAX_SAFE_INTEGER; // 最大安全整数
    let max1 = max + 1;
    let max2 = max + 2;
    max1 === max2; // true 超出最大值,计算出错
    
    // BigInt不用new可以直接使用,但不能和普通的数值相加
    // let bi1 = 9007199254740991n 与下一行代码效果一致,数字后加一个 n 是 BigInt 的字面量创建方式
    let bi = BigInt(Number.MAX_SAFE_INTEGER);
    let max3 = bi + 1n;
    let max4 = bi + 2n;
    max3 === max4; // false
    

1.数组

​ 数组可以容纳任何类型的值,可以是字符串、数字、对象或者是其他数组。

​ 创建稀疏数组(sparse array,即含有空白或者空缺单元的数组)时要特别注意

var a = [];
a[0] = 1;
// 没有设置a[1]的值
a[2] = 3;
a[1]; // undefined 
a.length; // 3 即使没有设置索引的值,仍然会占据索引位置

​ 数组通过数字进行索引,但数组也是对象,也可以包含字符串键值和属性值(但这些计算在数组长度内)

var a = [];

a[0] = 1;
// 相当于给a对象添加了一个名为footbar的属性,属性值为2
a['foobar'] = 2;
// 不会影响到同为对象属性的length的值
a.length; // 1
a['foobar']; // 2
a.foobar; // 2

// 如果字符串键值能被强制类型转换为十进制数字并且在合法范围内(0~2的32次方),它会被当做数字索引处理
var b = [ ];
b["13"] = 42;
b[0]; // undefined
b.length; // 14

​ 数组的方法

- Array.of(x,y,z...):将所有参数值组成数组
let arr = Array.of(1, 2, 3, 4, 5, 6); // [1, 2, 3, 4, 5, 6]
arr = new Array(1, 2, 3, 4, 5, 6); // [1, 2, 3, 4, 5, 6]

- arr.at(index) 获取指定索引位置的数组元素,index为数字,不过不是数字 会自动转为数字
arr.at(0); // 1
arr.at(-1); // 获取最有一个元素 6
// 当index为null undefined NaN时,默认取数组第一个元素
arr.at(null/undefined/NaN); // 1
// 传入的如果是正浮点数,则会向下取整
arr.at(1.4)  // 2
arr.at(1.7) // 2
// 传入的如果是负浮点数,则会向上取整
arr.at(-1.4) // 6
arr.at(-1.8) // 6
arr.at(-0.7) // 1

// 接下来的方法的arr都是独立测试的 彼此之间没有影响
- array.length:数组的长度
arr.length; // 6

- array.indexOf()和array.lastIndexOf() 返回数组中指定元素第一次和最后一次出现的索引位置
arr.indexOf(1); // 0
arr.lastIndexOf(1); // 0

- array.toString()  | array.join( '' ) :把数组转换成字符串,参数为分隔符
arr.toString(); // '1,2,3,4,5,6'
arr.join(','); // '1,2,3,4,5,6'

- array.pop( ) : 删除数组最后一个元素, 返回被删除的元素
arr.pop(); // 6;

- array.shift( ) : 删除数组的第一个元素,返回被删除的元素
arr.shift(); // 1

- array.push() :  在数组结尾处添加一个新的元素,返回新的数组的长度
arr.push(7); // 7
arr.length; // 7

- array.unshift( ) :  在数组开头添加一个新的元素,返回新的数组的长度
arr.unshift(0); // 7
arr.length; // 7

- array.splice(start, length, item1, item2, ... ) : 插入并删除元素 第一个参数定义新元素插入的位置,第二个参数定义删除的元素个数, 第三个参数定义插入的元素,返回删除的元素组成的数组, 原数组改变
// 删除元素
arr.splice(0, 1); // [1]
arr; // [2, 3, 4, 5, 6]
// 增加元素
arr.splice(0, 0, 0, 1); // []
arr; //  [0, 1, 1, 2, 3, 4, 5, 6]
// 替换元素
arr.splice(0, 1, 0); // [1]
arr; // [0, 2, 3, 4, 5, 6]

- arr1.concat(arr2, arr3, ... ) : 拼接数组
let arr2 = ['a', 'b' ,'c'];
arr.concat(arr2); //  [ 1, 2, 3, 4, 5, 6, 'a', 'b', 'c']

- array.slice( start, end ) : 裁剪数组 不会修改原数组,而是创建新数组
arr.slice(0, 2); // [1, 2]
arr; // [ 1, 2, 3, 4, 5, 6]

- array.sort( ) : 数组升序排列 参数为回调函数 sort函数是按照字母顺序也就是字符编码顺序进行排序,原数组改变
let arr1 = [12, 3, 1];
arr1.sort(); // [1, 12, 3]
arr1.sort((a,b) => a - b); // 1, 3, 12

- array.reverse( ) : 反转数组,原数组改变
arr.reverse(); // [6, 5, 4, 3, 2, 1]

- Math.max.apply(Math, array) | Math.min.apply(Math, array ) : 返回数组中的最大值和最小值
Math.max.apply(Math, arr); // 6
Math.min.apply(Math, arr); // 1

- array.forEach( function(value, index, array) ) :数组中每个元素调用一次函数, 原数组改变
arr.forEach((item, index, arr) => {
  arr[index] = item + 1;
})
arr; // [2, 3, 4, 5, 6, 7]

- array.map( function(value, index, array) ) : 数组中每个元素调用一次函数,返回一个包含结果的新数组, 原数组不变
let arr1 = arr.map((item, index, arr) => item * 2)
arr; // [1, 2, 3, 4, 5, 6]
arr1; // [2, 4, 6, 8, 10, 12]

- array.filter( function(value, index, array) ) : 返回一个通过函数测试的新的数组,原数组不变
let arr1 = arr.filter((item, index, arr) => item >= 3);
arr; // [1, 2, 3, 4, 5, 6]
arr1; // [3, 4, 5, 6]

- array.reduce( function(prev, cur, index, arr), initialValue  ): 在每个数组元素上运行函数,以生成(减少它)单个值,无initialValue 时,函数从第二个元素开始执行,perv = initialValue 
perv: 上一次回调函数的返回值或者是提供的初始值initialValue 
currentValue: 数组中当前被处理的元素
index: 当前元素在数组中的索引
array: 调用 reduce 的数组
arr.reduce((prev, cur) => prev + cur, 100); // 121

- array.every( function(value, index, arr) ) : 检查数组的所有元素是否都通过测试
arr.every((item, index, arr) => item > 3); // false

- array.some( function(value, index, arr) ) : 检查数组中是否有元素通过了测试
arr.some((item, index, arr) => item > 3); // true

- array.find( function(value, index, arr) ): 返回数组中第一个符合条件的元素
arr.find((item, index, arr) => item > 3); // 4

- array.findIndex( function(value, index, arr) ): 返回数组中第一个符合条件的元素的索引
arr.findIndex((item, index, arr) => item > 3); // 3

- array.fill(x, y, z): 用一个固定值替换数组的元素 x:用来替换的值 y: 被替换的起始索引 z: 被替换的结束索引,默认为数组末尾 负数表示倒数, 原数组改变
arr.fill(7); // [7, 7, 7, 7, 7, 7]
arr.fill(7, 3); //   [1, 2, 3, 7, 7, 7]
arr.fill(7, 0, 3); //  [7, 7, 7, 4, 5, 6]
arr.fill(7, 0, -4); //  [7, 7, 3, 4, 5, 6]

- array.copyWithin(x, y, z):从数组的指定位置拷贝元素到数组的另一个指定位置中  x:复制到的位置  y:复制内容的起始位置, 默认为0 z:复制内容的结束位置 默认数组末尾 负数表示倒数 原数组改变
arr.copyWithin(1); // [1, 1, 2, 3, 4, 5]
arr.copyWithin(1, 3); // [1, 4, 5, 6, 5, 6]
arr.copyWithin(1, 3, 5); // [1, 4, 5, 4, 5, 6]
arr.copyWithin(2, 0, -3); // [1, 2, 1, 2, 3, 6]

- entries() 键值对 keys() 键 values() 值
arr.entries(); // [object Array Iterator]
for(let entries of arr.entries()) {}; // [0, 1][1, 2][2, 3][3, 4][4, 5][5, 6]
arr.keys(); // [object Array Iterator]
for(let key of arr.keys()) {}; // 0, 1, 2, 3, 4, 5
arr.values(); // [object Array Iterator]
for(let value of arr.values()) {}; // 1, 2, 3, 4, 5, 6

- array.includes(x) : 返回boolean值
array.includes(2); // true
array.includes(0); // false

- array.flat() : 嵌套数组转一维数组 自动跳过空位 默认展开一层 返回一个新数组 原数组不变
let arr = [ 1, 2, 3, [], 4, [[5, 6]]];
arr.flat(); // [ 1, 2, 3, 4, [5, 6]];
arr.flat(2); // [ 1, 2, 3, 4, 5, 6];
arr.flat(Infinity); // [ 1, 2, 3, 4, 5, 6];

- array.flatMap(): 对数组中每个元素处理,再对数组执行flat()方法, 只能展开一层
arr.map((item) => [item * 2]); // [[2], [4], [6], [8], [10], [12]]
arr.flatMap((item) => [item * 2]) // [2, 4, 6, 8, 10, 12]
arr.map((item) => [[item * 2]]); //  [[[2]], [[4]], [[6]], [[8]], [[10]], [[12]]]
arr.flatMap((item) => [[item * 2]]) // [[2], [4], [6], [8], [10], [12]]

2.类数组

​ 比如一些DOM查询的DOM元素列表,函数的参数列表arguments对象(ES6已经废除)

类数组中必须包含length属性

类数组的key都是数字或者数字的字符串

无法使用数组丰富的内建方法,可以使用call() bind() apply() 改变this指向 来使用数组的内建方法

// 类数组转数组
1.for循环
2.Array.form()
3.[...CArr] 展开运算符
4.Array.prototype.slice.call(CArr)

3.字符串

​ 字符串的方法

var a = 'fool';
var b = ['f', 'o', 'o', 'l'];
- 字符串a的展开
[...a]; // ['f', 'o', 'o', 'l']

- JavaScript中字符串是不可变的,数组是可变的
a[1] = 'O';
b[1] = 'o';
a; // "fool"
b; // ['f', 'O', 'o', 'l']

- 所有字符串的方法都是返回新的字符串,不会修改原字符串
- str.length: 返回字符串长度
a.length; // 4
b.length; // 4

- str.indexOf(' ', start) : 返回字符串中指定文本首次出现的索引(位置),可以指定开始搜索的位置 
a.indexof('o'); // 1
b.indexOf('o', 2); // 2

- str.lastIndexOf('',  start ) : 返回字符串中指定文本最后一次出现的索引(位置),指定搜索位置是从后向前搜索
a.lastIndexOf('o'); // 2
b.lastIndexOf('o', 2); // 2

- str.search(''): 搜索特定值的字符串,并返回匹配的位置,可以搜索正则表达式的形式
- str.match('') : 根据正则表达式在字符串中搜索匹配项,并把结果作为Array返回
a.match('ool'); // ['ool']

- str.slice(start, end)  |  str.substring(start, length) | str.substr(start, length) :截取字符串 原字符串保持不变
a.slice(1, 3); // 'oo'
a.substring(0, 5); // 'fool'
a.substr(1, 3); // 'ool'

- str.replace(oldVal, newVal)   :替换字符串中的内容(首次匹配到的内容),原字符串保持不变
- str.replaceAll(oldVal, newVal) : 替换字符串中的所有匹配到的内容,原字符串保持不变
a.replace('o', 'a'); // 'faol'
a.replaceAll('o', 'a'); // 'faal'

- str.toUpperCase()  str.toLowerCase() : 转换大小写,原字符串保持不变
a.toUpperCase(); // 'FOOL'
a.toLowerCase(); // 'fool'

- str1.concat('', str2, str3, str4 ... ) :  连接字符串  第一个参数为连接符
a1 = 'My';
a2 = 'name';
a3 = 'is';
a4 = 'Lucian';
a1.concat(' ', a2, a3, a4); // 'My nameisLucian'
a1.concat(' ', a2,' ', a3, ' ', a4); // 'My name is Lucian'

- str.trim() : 去除字符串两端空格
- str.charAt(position) | str.charCodeAt(position) : 提取指定位置的字符/unicode编码
a.charAt(1); // 'o'
a.charCodeAt(1); // 111

- str.split(' ') : 按指定的分隔符将字符串分割成数组,参数为分隔符
a.split(); // ['fool']
a.split(''); // ['f', 'o', 'o', 'l']

- str.includes('', start) : 指定位置开始字符串是否包含指定值,返回true
a.includes('o', 0); // true
a.includes('a', 0); // false
a.includes('o', 4); // false

- str.startsWith('', start) : 判断字符串是否以指定值开始
- str.endsWith('', start) : 判断字符串是否以指定值结束
a.startsWith('o', 0); // false
a.startsWith('o', 1); // true

- str.padStart(length, str) 和 str.padEnd(length, str) : 支持在字符串的开头和结尾进行填充,第一个参数是指定生成的字符串的最小长度,第二个参数是用来补全的字符串
a.padStart(3, 'is');  'fool'   a.padEnd(3, 'is');  'fool' 
a.padStart(5, 'is');  'ifool'   a.padEnd(5, 'is');  'fooli' 
a.padStart(6, 'is');  'isfool' a.padEnd(6, 'is');  'foolis'
a.padStart(7, 'is');  'isifool' a.padEnd(7, 'is'); // 'foolisi'
a.padStart(6, '');  'fool' a.padEnd(6, '');  'fool'
a.padStart(6);  '  fool' a.padEnd(6); 'fool  '

4.数字

​ 数字和数值的方法,不会影响原数字

// 小数点前面只有0可以省略,小数部门最后面的0也可以省略
var a = 0.42;
var b = .42;
var c = 42.0;
var d = 42.

- num.toString() : 以字符串返回数字
var a = 123456;
a.toString(); // '123456'

- num.toExponential(x) : 把对象的值转换为指数计数法, x规定指数计数法中的小数位数,是 0 ~ 20 之间的值,包括 020,默认是0,返回值是String类型
var a = 100000000; // 1e8
var b = 123550000;
a.toExponential(); // '1.00e+8'
b.toExponential(2); // '1.23e+8'

- num.toFixed(x) : 返回字符串值,它包含了指定位数x小数的数字 (四舍五入)
- num.toPrecision(x) : 返回字符串值,它包含了指定长度x的数字 (四舍五入), x取值范围0 ~ 100
var a = 33.46;
a.toFixed(0);  '33'  a.toPrecision(0) 'RangeError'
a.toFixed(1);  '33.5'  a.toPrecision(1) '3e+1'
a.toFixed(2);  '33.46'  a.toPrecision(2) '33'
a.toFixed(3);  '33.460'  a.toPrecision(3) '33.5'
a.toFixed(4);  '33.4600'  a.toPrecision(4) '33.46'
a.toFixed(5);  '33.46000'  a.toPrecision(5) '33.460'


- num.valueOf( ) : 以数值返回数字
- Number( )、parseInt( )、 parseFloat( ) 将变量转换为数字
var a = 33.46;
var b = '33.46';
a.valueOf(); 33.46    b.valueOf(); '33.46'
Number(a); 33.46 		  Number(b); 33.46
parseInt(a); 33   		parseInt(b);  33
parseFloat(a); 33.46  parseFloat(b); 33.46

- Math.round(x) : 返回x四舍五入后的整数

- Math.ceil(x) : 返回x向上取整后的整数

- Math.floor(x) : 返回x向下取整后的整数

- Math.pow(x, y) : 返回x的y次幂

- Math.sqrt(x) : 返回x的平方根

- Math.abs(x) : 返回x的绝对值

- Math.min( ) 和 Math.max( )

- Math.random( ) : 生成0-1之间的随机数

- Math.cbrt(x): 返回x的立方根 会对非数值转化成数值计算

- Math.hypot(x,y,z.....) : 返回所有参数平方和的平方根 会对非数值转化成数值计算

- Math.trunc(x): 返回x的整数部分  会对非数值转化成数值计算

- Math.sign(x): 判断x的正 负 0 会对非数值转化成数值计算

- Math.expm1(x): 计算e的x次方-1

- Math.log10(x): 计算以10为底x的对数

- Math.sinh(x): 用于计算双曲正弦。

- Math.cosh(x): 用于计算双曲余弦。

- Math.tanh(x): 用于计算双曲正切。

- Math.asinh(x): 用于计算反双曲正弦。

- Math.acosh(x): 用于计算反双曲余弦。

- Math.atanh(x): 用于计算反双曲正切

- 指数运算符  从右向左计算
   1 ** 2 = 12次方 = 1
   2 ** 2 ** 3 = 2 ** 2的三次方 = 28次方 = 256

最大/小整数最大/小浮点数以及整数/安全整数的判断

Number.MAX_VALUE // 最大浮点数 1.798e+308
Number.MIN_VALUE // 最小浮点数 5e-324
Number.MAX_SAFE_INTEGER // 最大整数 2^53 - 1(9007199254740991)
Number.MIN_SAFE_INTEGER // 最小整数 -9007199254740991
// 判断一个值是否为整数
Number.isInteger( 42 ); // true
Number.isInteger( 42.000 ); // true
Number.isInteger( 42.3 ); // false

// 判断一个值是否为安全的整数
Number.isSafeInteger( Number.MAX_SAFE_INTEGER );  // true
Number.isSafeInteger( Math.pow( 2, 53 ) );  // false
Number.isSafeInteger( Math.pow( 2, 53 ) - 1 );  // true

判断0.1+0.2和0.3是否相等

// 通过比较两个数差值的绝对值是否小于机器精度2^-52 (2.220446049250313e-16)来判断
function numbersCloseEnoughToEqual(n1,n2) {
    return Math.abs( n1 - n2 ) < Number.EPSILON;
}

var a = 0.1 + 0.2;
var b = 0.3;

numbersCloseEnoughToEqual( a, b ); // true
numbersCloseEnoughToEqual( 0.0000001, 0.0000002 ); // false

5.特殊数值

undefinednull

- null 空值,曾赋过值,但目前没有值,null是一个特殊关键字,不能当做变量来使用和赋值,布尔运算中为false
-undefined 没有值,也没赋过值,可以当做全局变量来使用和赋值(最好不要这么做)
// 进行计算时null可以视为0
1 + null  // 1
1 - null // 1
1 * null // 0
120 / null // Infinity(JS中分母可以为 0)
-120 / null // -Infinity
0 / 0 // NaN
任何数 % 0 // NaN

// 注意null和undefined比较相等时的区别
typeof null // 'object'
typeof undefined // 'undefined'
null == undefined // false
null === undefined // true
isNaN(1 + null) // false
isNaN(1 + undefined) // true
Number(null); // 0
Number(undefined); // undefined

// 使用void运算符可以返回undefined 
var a = 42;
void a; // undefined
a; // 42

NaN

- NaN 是一个表示非数字的值,是全局作用域中的一个变量,布尔运算中为false

- 请注意 isNaN() 和 Number.isNaN() 之间的区别:如果当前值是 NaN,或者将其强制转换为数字后将是 NaN,则前者将返回 true。而后者仅当值当前为 NaN 时才为 trueisNaN("hello world"); // true 会进行强制数字类型转换
Number.isNaN("hello world"); // false

- 有五种不同类型的操作返回 NaN:
  - 失败的数字转换(例如,显式转换,如 parseInt("blabla")、Number(undefined),或隐式转换,如 Math.abs(undefined))
  - 计算结果不是实数的数学运算(例如,Math.sqrt(-1))
  - 不定式(例如,0 * Infinity1 ** InfinityInfinity / InfinityInfinity - Infinity)
  - 一个操作数被强制转换为 NaN 的方法或表达式(例如,7 ** NaN7 * "blabla")——这意味着 NaN 具有传染性
  - 将无效值表示为数字的其他情况(例如,无效的 Date new 	Date("blabla").getTime()、"".charCodeAt(1))

如果 NaN 涉及数学运算(但不涉及位运算),结果通常也是 NaN。
当 NaN 是任何关系比较(>, <, >=, <=)的操作数之一时,结果总是 falseNaN 不等于(通过 ==、!=、=== 和 !==)任何其他值——包括与另一个 NaN 值。

//NaN是唯一不与自身相等的值
NaN === NaN // false

-0

加法和减法运算不会得到负零
// 负零在开发调试控制台中通常显示为 -0,但在一些老版本的浏览器中仍然会显示为 0
var a = 0 / -3; // -0
var b = 0 * -3; // -0

-0从数字转为字符串会得到"0"
var a = -0;
a.toString(); // "0"
a + ""; // "0"
String( a ); // "0"
JSON.stringify( a ); // "0"

-"-0"从字符串转换为数字,会得到正确的结果
+"-0"; // -0
Number( "-0" ); // -0
JSON.parse( "-0" ); // -0

-0 == 0  // true
-0 === 0 // true
"-0" === "0" // false

6.日期

var date = new Date(); // Mon Mar 13 2023 17:03:24 GMT+0800 (中国标准时间)
- date.getTime() // 1678698177256 获取自 1970 年 1 月 1 日以来的毫秒数 

- date.getFullYear() // 2023 以四位数字形式返回日期年份  
- date.getMonth() // 2 以数字(0-11)返回日期的月份 
- date.getDate() // 13 以数字(1-31)返回日期的日 
- date.getDay() // 1 以数字(0-6)返回日期的星期名(weekday) 
- date.getHours() // 17 以数字(0-23)返回日期的小时数 
- date.getMinutes() // 8 以数字(0-59)返回日期的分钟数 
- date.getSeconds() // 13 以数字(0-59)返回日期的秒数 
- date.getMilliseconds() // 317 以数字(0-999)返回日期的毫秒数 
方法描述
setDate()以数值(1-31)设置日
setFullYear()设置年(可选月和日)
setHours()设置小时(0-23)
setMilliseconds()设置毫秒(0-999)
setMinutes()设置分(0-59)
setMonth()设置月(0-11)
setSeconds()设置秒(0-59)
setTime()设置时间(从 1970 年 1 月 1 日至今的毫秒数)

声明变量的三种方式 let var const

var

  • 全局作用域或者函数作用域
  • 可以重新赋值和重新声明
  • var具有变量提升,初始化为undefined,无暂时性死区, 声明前使用为undefined
  • 函数作用域中未使用var声明的的变量会成为全局变量

let

  • 块级作用域 for语句 if语句 函数等快代码块
  • 可以重新赋值但不能重新声明
  • let具有变量提升, 无初始化,但是具有暂时性死区, 声明前使用会报ReferenceError: Cannot access 'a' before initialization

const

  • 块级作用域
  • 定义常量,不可以重新赋值,也不能重新声明
  • let具有变量提升, 无初始化,但是具有暂时性死区, 声明前使用会报ReferenceError: Cannot access 'a' before initialization

封装对象的包装和拆包

1.包装类

  • 为了方便操作基本类型值,JS提供了三个特殊的引用类型:BooleanNumberString(undefinednull没有自己的包装类),每当读取一个基本类型值时,后台就会创建一个对应的基本包装类型的对象;

  • 所有的包装类都有valueOf()toString()方法,还有charAt()、indexOf()等方法;

    valueOf(): 返回对象的字符串,数值或布尔值表示

    toString(): 返回对象的字符串表示

    let a = 10;
    let b = '10';
    let c = false;
    let d = { test: '这是测试值' };
    let e = ['m', 'n', 'o'];
    // 操作对象的时候,如果有valueOf()会先调用valueOf()方法,然后调用Number()方法进行转换,如果只有toString()方法,就调用toString()方法
    console.log(a.valueOf()); //10
    console.log(b.valueOf()); //'10'
    console.log(c.valueOf()); //false
    console.log(d.valueOf()); //{test: "这是测试值"}
    console.log(e.valueOf()); //(3) ["m", "n", "o"]
    
    console.log(a.toString()); //'10'
    console.log(b.toString()); //'10'
    console.log(c.toString()); //'false'
    console.log(d.toString()); //'[object Object]'
    console.log(e.toString()); //'m,n,o'
    
  • 原始值不是对象,无法拥有自己的属性,但因为包装类的存在,原始值好似拥有了自己的属性;

2.包装的实现

  • 使用JS内置的原生构造器 new 包装类() 构造函数只接受一个参数,参数会被转换成包装类的类型

    new String()、 new Number()、 new Boolean()

    var a = new String('abc');
    var b = 'abc';
    a == b; // true 这里为true的原型是因为使用了隐式转换,a调用了字符串的toString方法
    a === b; // false
    a.toString(); // 'abc'
    a.valueOf(); // 'abc'
    
  • 直接使用Object()封装对象的包装,给Objec()方法传入不同的参数类型(nulundefined除外),就会返回不同类型的包装类。

    var a = 'abc';
    var b = new String( 'abc' );
    var c = Object( 'abc');
    // Object(value) 和 new Object(value)得到的结果是等价的,但是它们的语义是不同的。前者表示将value转成一个对象,后者是表示生成一个对象,它的值是value。
    typeof a; // "string"
    typeof b; // "object"
    typeof c; // "object"
    
    a instanceof String; // false
    b instanceof String; // true
    c instanceof String; // true
    
    Object.prototype.toString.call(b); // "[object String]"
    Object.prototype.toString.call(c); // "[object String]"
    
    b == c // false, 因为地址不一样
    b === c // faslse, 因为地址不一样
    
    a == b // true,触发隐式转换,b会调用valueOf()方法
    a === b // false,不会触发隐式转换
    
    `如果参数是原始类型的值,Object方法会将其转为对应的包装对象的实例`
    
    var obj = Object(1)
    obj instanceof Object // true
    obj instanceof Number // true
    typeof(obj) // 'object'
    
    var obj = Object('foo')
    obj instanceof Object // true
    obj instanceof String // true
    typeof(obj) // 'object'
    
    var obj = Object(true)
    obj instanceof Object // true
    obj instanceof Boolean // true
    typeof(obj) // 'object'
    
    `方法的参数是一个对象,它总是返回该对象,即不用转换。`
    
    var arr = []
    var obj = Object(arr) // 返回原数组
    obj === arr // true
    
    var value = {}
    var obj = Object(value) // 返回原对象
    obj === value // true
    
    var fn = function () {}
    var obj = Object(fn) // 返回原函数
    obj === fn // true
    
    `判断给定数据是否为对象类型:`
    方法一:
    function isObject(value) {
      return value === Object(value)
    }
    
    isObject([]) // true
    isObject(true) // false
    
    方法二:
    let obj = {}
    Object.prototype.toString.call(obj) === '[Object Object]'
    
    方法三:
    let obj = {}
    obj.constructor === Object
    
    方法四:
    let obj = {}
    obj instanceof Object
    

3.拆包的实现

  • 显式拆包:包装类.valueOf()

    var a = new String( "abc" );
    var b = new Number( 42 );
    var c = new Boolean( true );
    
    a.valueOf(); // "abc"
    b.valueOf(); // 42
    c.valueOf(); // true
    
  • 隐式拆包:利用一元运算符+

    var a = new String( "abc" );
    var b = a + ""; // b的值为"abc"
    
    typeof a; // "object"
    typeof b; // "string"
    

原型和原型链

1.原型

每个函数(对象)都有一个自己的prototype原型属性,默认是一个object空对象(函数的原型对象)

原型对象中存在constructor__proto__([[Prototype]])属性

原型对象转存失败,建议直接上传图片文件

  • 隐式原型: 每一个实例对象都有一个__proto__属性,创建对象时自动添加,指向其构造函数的prototype对象
  • 显示原型:原型prototype只存在于**(构造)函数**中,存储共享的属性和方法
  • 构造器:constructor属性只存在于**(构造)函数prototype中,指向(构造)函数**本身

2.原型链

当访问一个对象的某个属性时,如果这个对象本身没有这个属性,那么它会去它的隐式原型 __proto__(也就是它的构造函数的显式原型 prototype)中寻找,如此递推下去,一直检索到Object对象,这个寻找的过程就形成了原型链的概念。

原型链示意图转存失败,建议直接上传图片文件

原型链中一些需要注意的点

// 引用类型和函数对象的原型对象
typeof Number.prototype // 'object'
typeof String.prototype // 'object'
typeof Boolean.prototype // 'object'
typeof Array.prototype // 'object'
typeof Object.prototype // 'object'
typeof Function.prototype // 'function'

// 一切皆对象,任何构造函数都是对象的一种,都有__proto__属性,生成这些构造函数的就是Function
Number.__proto__ === Function.prototype; // true
String.__proto__ === Function.prototype; // true
Boolean.__proto__ === Function.prototype; // true
Array.__proto__ === Function.prototype; // true
Object.__proto__ === Function.prototype; // true
Function.__proto__ === Function.prototype; // true

// 任何构造函数都是Function(构造函数)的实例对象,他们的constructor都指向Function
Number.constructor; // ƒ Function() { [native code] } 'function'
String.constructor; // ƒ Function() { [native code] } 'function'
Boolean.constructor; // ƒ Function() { [native code] } 'function'
Array.constructor; // ƒ Function() { [native code] } 'function'
Object.constructor; // ƒ Function() { [native code] } 'function'
Function.constructor; // ƒ Function() { [native code] } 'function'

// 构造函数原型对象上的constructor方法指向构造函数本身
Number.prototype.constructor; // ƒ Number() { [native code] } 'function'
String.prototype.constructor; // ƒ String() { [native code] } 'function'
Boolean.prototype.constructor; // ƒ Boolean() { [native code] } 'function'
Array.prototype.constructor; // ƒ Array() { [native code] } 'function'
Object.prototype.constructor; // ƒ Object() { [native code] } 'function'
Function.prototype.constructor; // ƒ Function() { [native code] } 'function'

// 原型链的顶端是Object
Object instanceof Function // 任何构造函数都是Function的实例
Function instanceof Object // 一切皆对象

this关键字

基础指向问题

  • ES5中,this永远指向最后调用它的那个对象,如果没有调用对象的话,this指向全局对象window(严格模式下全局对象window为undefined),this是在执行过程中绑定的

  • ES6的箭头函数中,this始终指向函数定义时的this,也就是声明时所在上下文(作用域)的this,箭头函数中的this不能修改和指定

  • 而setTimeOut函数中的第一个参数是方法,传入这个方法内部中的this会被改写成window(window.setTimeout()),如果参数是箭头函数,那箭头函数中的this指向setTimeOut声明时所在的作用域的this

  • 函数的形式调用时,this永远都是window,匿名函数大多时候this都指向window 以方法的形式调用时,this就是调用方法的对象 以构造函数的形式调用时,this就是新创建的对象 使用call、apply和bind调用时,this就是指定的那个对象

  • 事件绑定函数中的this:谁调用函数,this指向谁

  • this只会在函数作用域全局作用域,不要与块级作用域混淆

call,apply,bind的原理和实现

call,apply和bind都是Function原型上的方法Function.prototype.call()Funciton.prototype.apply()Function.prototype.bind(),都是用来改变当前函数的this指向,第一个参数都是this;

函数调用call和apply方法时,返回的是调用函数的返回值,而bind返回的是一个新的函数

call接受的是一系列参数,apply接受的是一个数组

三者的区别(调用三者的一定是函数或方法)

方法.call(this指向对象,参数1,参数2,...,参数N) 方法.apply(this指向对象,[参数1,参数2,...,参数N]) 方法.bind(this指向对象,参数1,参数2,...,参数N)()

  • call和apply的实现(apply不再赘述)

    • 如果没有指定context,this默认指向window;
    • 给传入的上下文对象添加一个当前function的属性;
    • 执行新属性的方法并在执行结束后删除该方法,返回方法执行结果
Function.prototype.myCall = function(context, ...args) {
  context = context || window;
  // 为了防止fn和context的原有属性重复,使用symbol()定义
  var fn = Symbol('fn');
  // this就是Function context就是传入的上下文对象
  // fn这里是Symbol变量,要用中括号形式获取属性
  context[fn] = this;
  // apply写法 var res = context.fn(...args[1]);
  var res = context[fn](...args);
  delete context.fn;
  return res;
}
Function.prototype.myApply = function(context, ...args) {
  // apply判断args的类型,如果不是Array的实例,抛出一个TypeError;
  if(!(args instanceof Array)){
    throw new TypeError(`args is not an array!`)
  }
  context = context || window;
  // 为了防止fn和context的原有属性重复,使用symbol()定义
  var fn = Symbol('fn');
  // this就是Function context就是传入的上下文对象
  // fn这里是Symbol变量,要用中括号形式获取属性
  context[fn] = this;
  var res = context[fn](...args[1]);
  delete context.fn;
  return res;
}

function foo() {
 console.log(this.name);
}

var obj = {
 name: 'liu',
}

foo.myCall(obj);
  • bind实现
Funciton.prototype.myBind = function(context, ...args1) {
 var that = this;
 return function(...args2) {
   return that.call(context, ...args1, ...args2)
 }
}

new关键字的实现

  1. 创建一个新的空对象obj{}
  2. 将新对象obj的内部属性(proto_)指向构造函数的原型prototype,这样新对象就可以访问构造函数原型中的方法和属性;
  3. 将构造函数中this指向新对象obj,并得到构造函数的返回值
  4. 根据返回值的类型判断,如果返回值是对象就返回该对象,否则返回构造函数的一个实例对象也就是obj
function _new(Constructor, ...arguments) {
  // 分别对应上述4步
  var obj = {};
  obj.__proto__ = Constructor.prototype;
  var res = Constructor.apply(obj, arguments);
  // var res = Constructor.call(obj, ...arguments);
  return typeof res === 'object' ? res : obj;
}

function Student(name, age) {
  this.name = name;
  this.age = age;
}

var student = _new(Student, 'liu', 26);
console.log(student); //  [object Object] { age: 26, name: "liu" }预编译

预编译过程

参考 一看就懂的作用域和作用域链 - 掘金 (juejin.cn)

					[JavaScript预编译过程_js预编译过程_five-five的博客-CSDN博客](https://blog.csdn.net/qq_45074341/article/details/122758374) 

js代码运行的三个阶段

  1. 词法语法分析:检查JavaScript代码是否存在一些低级的语法错误;
  2. 预编译:构建各种运行需要的环境;
  3. 执行代码:js引擎解析代码,解析一行执行一行。

预编译发生在两个时间点:

  • 第一个是发生在JavaScript代码执行前(全局预编译);
  • 第二个是发生在函数执行前(局部预编译);

全局预编译只发生一次,局部预编译在每次函数执行之前都会发生。

全局预编译

  1. JavaScript代码执行之前,首先会创建一个全局对象,可以理解为window对象,也可以理解为GO(Global Object)对象,无法打印;
  2. 然后所有声明的全局变量、未使用varlet声明的变量放到GO对象中,成为GO对象的属性,并且赋值为undefined
  3. 再将所有的函数function声明也放到GO对象中,成为GO对象的属性,属性名为函数名,值为函数体,如果函数名和变量名相同,函数名覆盖变量名

局部预编译

函数调用执行前产生,生成自己的作用域AO(Activation Object)活动对象,如果有多个函数调用,会产生多个AO。函数的作用域是在预编译阶段决定的,不管你在什么位置调用函数,他的上级函数作用域是不变的。

  1. 生成AO活动对象;
  2. 形参放到AO对象中,成为AO对象的属性,值为实参的值,若未传值,则为undefined;
  3. var关键字声明的变量放到AO对象中,成为AO对象的属性,值为undefined,若重复,则不发生变化;
  4. 将所有function的函数声明放到AO对象中,成为AO对象的属性,属性名为函数名,如果函数名和变量名形参名重复,函数名覆盖变量名。

执行上下文的生命周期

创建阶段 -> 执行阶段 -> 回收阶段

执行上下文是在代码运行时确定(比如this的指向就是执行时确定的),随时可以改变;作用域在定义时就会确定并且不会改变。

1.创建阶段

  • 绑定当前上下文的this,在全局执行上下文中,this 的值指向全局对象。(在浏览器中,this引用 Window 对象)。 在函数执行上下文中,this 的值取决于该函数是如何被调用的。如果它被一个引用对象调用,那么 this 会被设置成那个对象,否则 this 的值被设置为全局对象或者 undefined(在严格模式下)
  • 创建词法环境(LexicalEnvironment component)
  • 创建变量环境(VariableEnvironment component)

2.执行阶段

  • 执行变量赋值,代码执行

3.回收阶段

  • 执行上下文出栈并等待虚拟机回收

闭包

1.变量作用域

变量根据作用域的不同分为两种: 全局变量和局部变量。

  1. 函数内部可以使用全局变量。
  2. 函数外部不可以使用函数内部的局部变量。
  3. 当函数执行完毕,本作用域的局部变量会销毁。
  4. 函数内部没有使用关键词声明的变量其实是个全局变量

2.闭包

  • 闭包的定义:有权访问另一个函数作用域中变量的函数,就是一个作用域中的函数可以访问到另一个函数内部的局部变量

  • 假如一个函数A返回了一个函数B,函数A执行对象AO中保存了函数B的堆内存地址,函数B中引用了函数A中的变量a,全局对象GO中有变量C接收了函数B,C保存了指向函数B的堆内存地址。C -> B -> A 因为引用存在,堆内存中的函数B不能被回收,函数A中的基础变量a不会被回收,函数A的执行对象AO也不会被回收。

function createAdder(count) { function adder(num) { return count + num; } return adder } var adder1 = createAdder(5) adder1(1); var adder2 = createAdder(10) adder2(1);




- ![闭包](D:\OneDrive\图片\闭包.jpg)

- 闭包的作用:延伸了变量的作用范围, 闭包中的基本数据类型变量是保存在堆内存里的,当函数执行完返回一个内部函数的一个引用,引用不能释放,因此当前执行上下文也不能被释放,这时候函数的变量就会转移到堆上,因此内部函数依然能访问到上一层函数的变量。 
1. 保护:划分一个独立的代码执行区域,在这个区域中有自己私有变量存储的空间,而用到的私有变量和其它区域中的变量不会有任何的冲突(防止全局变量污染)
2. 保存:如果上下文不被销毁,那么存储的私有变量的值也不会被销毁,可以被其下级上下文中调取使用

- 闭包的特点:
1. 闭包一定有嵌套函数(内层函数)
2. 外层函数一定有局部变量,且内层函数一定引用操作了这个变量
3. 会使用`return`将内层函数返回
4. 外部执行函数中的`this`指向`window`
5. 函数内部的**匿名函数**本身也是一个闭包

```js
// 直接在外层函数中返回匿名内层函数,全局作用域中的f()可以访问到fn()中的局部变量a:
function fn() {
var a = 10;
return function() {
console.log(a, this === window);
}
}

var f = fn();
f(); // 10 true

数学运算符中的类型转换

因为 JS 并没有类型声明,所以任意两个变量或字面量,都可以做加减乘除。

1. 减、乘、除

⭐️我们在对各种非Number类型运用数学运算符(- \* /)时,会先将非Number类型转换为Number类型。

1 - true // 0, 首先把 true 转换为数字 1, 然后执行 1 - 1
1 - null // 1,  首先把 null 转换为数字 0, 然后执行 1 - 0
1 * undefined //  NaN, undefined 转换为数字是 NaN
2 * ['5'] //  10, Number(['5'])变成数字 5

2. 加法的特殊性

⭐️为什么加法要区别对待?因为JS里 +还可以用来拼接字符串。谨记以下3条:

  • 当一侧为String类型,被识别为字符串拼接,并会优先将另一侧转换为字符串类型。
  • 当一侧为Number类型,另一侧为原始类型,则将原始类型转换为Number类型。
  • 当一侧为Number类型,另一侧为引用类型,将引用类型和Number类型转换成字符串后拼接。

⭐️以上 3 点,优先级从高到低,即 3+'abc' 会应用规则 1,而 3+true会应用规则2。

123 + '123' // 123123   (规则1)
123 + null  // 123    (规则2)
123 + true // 124    (规则2)
123 + {}  // 123[object Object]    (规则3)

// 特殊情况
// 当“+”号左右两边都是数值时,有以下特殊情况:
(1). 当有一个操作数是NAN,则结果是NAN;
(2). 如果是Infinity加上Infinity,则结果是InfinityInfinity表示无穷大);
(3). 如果是 - Infinity + -Infinity,则结果是-InfinityInfinity表示无穷大);
(4). 如果是 Infinity + -Infinity,则结果是 NaN;
// 空数组 + 空数组以及空对象 + 空对象
[] + [] // '',先调用valueOf方法,再调用toString()方法(toPrimitive),''
({}) + {} // "[object Object][object Object]",原理同上,如果对象不加括号,在不同浏览器会得到不同结果 主要是两种,"[object Object][object Object]" 以及 NaN
// 空对象+空数组 VS 空数组+空对象
{} + []  // {}被视为代码块,[]强制转换为number类型,得到0
({}) + [] //'[object Object]'
[] + {} //'[object Object]'
[] + {} == ({}) + []  // true,比较的是字符串
[] + {} === ({}) + [] // true

3.相等操作符的强制转换

ECMAScript 中的相等操作符由两个等于号 ( == ) 表示,如果两个操作数相等,则返回 true。

**相等操作符会先转换操作数(通常称为强制转型),**然后比较它们的相等性。

在转换不同的数据类型时,相等操作符遵循下列基本规则:

  1. 如果有一个操作数是布尔值,则在比较相等性之前,将其转换为数值;

  2. 如果一个操作数是字符串,另一个操作数是数值,在比较之前先将字符串转换为数值;

  3. 如果一个操作数是对象,另一个操作数不是,则调用对象的 valueOf() 方法,用得到的基本类型值按照前面的规则进行比较;

  4. 如果有一个操作数是 NaN,无论另一个操作数是什么,相等操作符都返回 false;

  5. 如果两个操作数都是对象,则比较它们是不是同一个对象。如果指向同一个对象,则相等操作符返回 true;

  6. 在比较相等性之前,不能将 null 和 undefined 转成其他值。

  7. null 和 undefined 是相等的。

前三条可以总结为当相等操作符两边的操作数不包含nullundefined,并且不全是对象时,会调用Number()将两个操作数强制转换为Number类型。

// js声明引用类型时,变量名会保存在栈内存中,对应的值保存在堆内存中,栈内存中其实保存的是值在堆内存中的地址
// 都为false的原因是两边操作数的引用地址不同
[] == []; // false
{} == {}; // false
[] === []; // false
{} === {}; // false

函数的一些小秘密

1.函数的长度length

// 无默认值时,函数的长度等于参数的个数
function fn0() {}; // fn0.length: 0
function fn1(a) {}; // fn1.length: 1
function fn2(a, b) {}; // fn2.length: 2
// 有默认值时,函数的长度是第一个具有默认值的参数之前的参数个数
function fn3(a = 0) {}; // fn3.length: 0
function fn4(a, b = 0, c) {}; // fn4.length: 1
// 有剩余参数 ...args, 剩余函数不计入函数长度内
function fn5(name, ...args) {}; // fn5.length: 1

2.函数的原型 prototype

// 声明一个函数   他是Function函数的实例对象
function fn() {};
fn instanceof Function; // true
fn instanceof Object; // true 函数也是对象
fn.constructor === Function; // true
fn.__proto__ === fn.prototype; // true

// 声明一个fn的实例对象f
var f = new fn();
f instanceof fn; // true
f instanceof Function; // false
f instanceof Object; // true
f.constructor === fn; // true
f.__proto__ === fn.prototype; // true
f.__proto__.__proto__ === Object.prototype; // true

3.函数的 name

这个属性的值永远等于跟在function关键字后面的标识符,匿名函数的name属性为空。 即:函数名称.name == 函数名称(除匿名函数)

4.函数的表达式

// 将一个匿名函数赋值给变量,这个匿名函数就被称为函数表达。
const hello = function (a) {
  console.log(a)
}

// 将一个具名函数赋值给变量
// 具名函数fn和hello是同一个函数,但是作用范围不一致,fn只能在函数体内使用,相当于函数的一个局部变量,hello可在函数内部,外部调用。

const hello = function fn(a) {
  console.log(a)
  console.log(fn)  // fn()
  console.log(fn === hello) // true
}
console.log(fn)  // ReferenceError: fn is not defined
函数在非严格模式下,this被绑定到全局对象。在严格模式下,thisundefined

5.函数的构造函数

// 可以传递任意数量的参数给Function构造函数,但只有最后一个参数会被当做函数体,如果只有一个参数,该参数就是函数体。

// 注意: 此处是否使用new,结果都一样
const add = new Function('x', 'y', 'return x + y')
等价于
function add(x, y) {
  return x + y
}

add(1, 2) // 3

注意: 如果构造函数没有实参,可以省略括号()
const o = new Object()
// 等价于
const o = new Object
Function.prototype.myCall = function(context, ...args) {
    context = context || window;
    context.fn = this;
    var res = context.fn(...args);
    delete context.fn;
    return res;
}

Function.prototype.myApply = function(context) {
    context = context || window;
    context.fn = this;
    var args = Array.prototype.slice.call(arguments,1);

    var res = context.fn(...arguments[1]);
    delete context.fn;
    return res;
}

Function.prototype.myBind = function(context, ...args1) {
    context = context || window;
    var that = this;
    return function(...args2){
        that.call(context, ...args1, ...args2)
    }
}



var obj1 = {
    name: 'liu'
}

var obj2 = {
    name: 'guo',
    fn1(age, sex) {
        console.log(this.name, age, sex)
    }
}

obj2.fn1.myCall(obj1, 18, 'famale');
obj2.fn1.myApply(obj1, [18, 'female']);
obj2.fn1.myBind(obj1, 18)('female');


function _new(Constructor, ...args) {
    var obj = {};
    obj.__proto__ = Constructor.prototype;
    var res  = Constructor.call(obj, ...args);
    return typeof res === 'object' ? res : obj;
}

function Student(name, age, sex) {
    this.name = name;
    this.age = age;
    this.sex = sex
}

var student = _new(Student, 'liu', 18, 'male');
console.log(student)

convert-table转存失败,建议直接上传图片文件