前言
如果你看完觉得没用,欢迎打死兔兔。
1. parseInt() 、toString() 的可选参数
parseInt(string, radix)
radix 可选。表示要解析的数字的基数。该值介于 2 ~ 36 之间。 如果省略该参数或其值为 0,则数字将以 10 为基础来解析。如果它以 “0x” 或 “0X” 开头,将以 16 为基数。如果该参数小于 2 或者大于 36,则 parseInt() 将返回 NaN。
eg:
parseInt('10', 2); // 2 1*2^1 + 0*2^0 = 2
parseInt('17', 8); // 15 1*8^1 + 7*8^0 = 15
parseInt('1f', 16); // 31 1*16^1 + 15*16^0 31
parseInt('0x12'); // 18 1*16^1 + 2*16^0 = 18
number.toString(radix)
radix 可选。规定表示数字的基数,是 2 ~ 36 之间的整数。若省略该参数,则使用基数 10。但是要注意,如果该参数是 10 以外的其他值,则 ECMAScript 标准允许实现返回任意值。
eg:
var num = 15;
num.toString(2); // 1111 1*2^3 + 1*2^2 + 1*2^1 + 1*2^0 = 15
num.toString(8); // 17 1*8^1 + 7*8^0 = 15
num.toString(16); // f f在16进制中即代表15(0~9,A~F)
2. 1.toString() 报错,为何不输出 "1" ?
var num = 1;
num.toString(); // "1"
Number(1).toString(); // "1"
1.toString(); // Uncaught SyntaxError: Invalid or unexpected token
1.0.toString(); // "1"
1..toString(); // "1"
1 .toString(); // "1"
原因:当点跟在一个数字后面就意味着这个数字是一个浮点数,在点后面JS等待着一个数字。
所以在调用toString()
之前,我们需要告诉JS这是就是我们要的数字。通过变量的形式调用toString()
,实际上是发生了隐式转换,number
属于基本类型,是没有toString()
方法的,隐式转换变为包装类型,所以不会报错。
3. 为何 0.1+0.2=0.30000000000000004 ?
解答:对于计算机而言,两个数字在相加时是以二进制形式进行的,在呈现结果时才转换成十进制。这样的结果是因为在转换的过程中发生了精度丢失,解决的办法是先转换为整数执行操作后再降下来,当然对于复杂的,可以引入第三方库(bignumber.js等)。
Number.MAX_VALUE; // 1.7976931348623157e+308
Number.MIN_VALUE; // 5e-324
4. 数组常用迭代方法的第二参数
这里的迭代方法有: every() 、 filter() 、 forEach() 、 map() 、 some() 、 find() 、 findIndex() 。
这些方法的所需参数都相同,以用的最多的forEach()
为例进行说明:
array.forEach(function(currentValue, index, arr), thisValue)
thisValue 可选。传递给函数的值一般用 "this" 值。如果这个参数为空, "undefined" 会传递给 "this" 值
eg:
function run(param) {
console.log( param, this.id );
}
var obj = {
id: "welcome"
};
[1, 2, 3].forEach( run, obj ); // 1 "welcome" 2 "welcome" 3 "welcome"
如果需要在迭代中显示绑定this
,不妨考虑下这个第二参数。
5. 数组最易被忽略的 reduce() 方法
array.reduce(function(total, currentValue, currentIndex, arr), initialValue)
①function(total,currentValue, index,arr)
- total 必需。初始值, 或者计算结束后的返回值。
- currentValue 必需。当前元素
- currentIndex 可选。当前元素的索引
- arr 可选。当前元素所属的数组对象。
②initialValue 可选。传递给函数的初始值
妙用——统计字符串中每个字符出现的次数,最简练的代码:
var str = 'welcome to my world';
var obj = {};
[...str].reduce((prev, cur)=>{
obj[cur] ? obj[cur]++ : obj[cur] = 1;
}, {})
console.log(obj); // {" ": 3,c: 1,d: 1,e: 2,l: 2,m: 2,o: 3,r: 1,t: 1,w: 2,y: 1}
6. 理解数组是特殊的对象
以定义对象的形式来定义数组,如下:
var arr = {
0: 1,
1: 2,
2: 3,
length: 3
}
arr[0]; // 1
typeof arr; // "object"
数组是特殊的对象,自然可以添加属性:
var arr = [1, 2, 3];
arr.md = 'well';
arr.length; // 3
arr.md; // "well"
// 注意点
arr["2"] = 4; // 这里有引号,本质上就算这里没有引号,也会先转成字符串形式,因为对象的键只能为字符串
arr[2]; // 4
7.数组的空位
在ES6出现后,正确新建指定长度数组的方式应该是Array.from({length: 3})
,还有一种繁琐的写法Array.apply( null, { length: 3 } )
,而在此之前新建数组的方式都有或多或少的问题。下面我们来通过代码看一下:
Array.from({length: 3}); // [undefined, undefined, undefined]
Array(3); // [empty × 3] 即 [, , ,]
Array(1, 2, 3); // [1, 2, 3]
对应上面3中方式,我写成对象的形式帮大家理解一下:
{0: undefined, 1: undefined, 2: undefined, length: 3}
{length:3}
{0: 1, 1: 2, 2: 3, length: 3}
所以Array.from({length: 3})
等价于Array.from(Array(3))
。在第二种建立数组的方式中,出现的即为数组的空位。第二种与第三种方式展现了因参数个数不同,导致Array()
的行为差异,当然ES6提供了解决方式Array.of()
,自行查阅,这里不做解释。我们重点在于空位:
- ES5 对空位的处理,表现的很不一致,大多数情况下会忽略空位。
- forEach(), filter(), reduce(), every() 和some()都会跳过空位
- map()会跳过空位,但会保留这个值
- join()和toString()会将空位视为undefined,而undefined和null会被处理成空字符串
- ES6 则是明确将空位转为
undefined
- Array.from方法会将数组的空位,转为undefined
- 扩展运算符(...)会将空位转为undefined
- copyWithin()会连空位一起拷贝
- fill()会将空位视为正常的数组位置
- for...of循环也会遍历空位
- entries()、keys()、values()、find()和findIndex()会将空位处理成undefined
可见空位的处理规则非常不统一,所以建议避免出现空位。
8. 字符串的截取方法 slice() 、substr() 、substring() 的区别
string.slice(start,end) 参数可以为负数
string.substr(start,length) 第一参数可为负数,第二参数为长度
string.substring(from, to) 参数都必须为非负整数
9. setTimeout() 第三参数
作用:第三参数会作为第一个函数的参数传入
对比下面代码,见识一下作用:
for(var i = 0; i<6; i++){
setTimeout(function(){
console.log(i);
},1000);
} // 输出6次6
for(var i=0;i<6;i++){
setTimeout(function(j){
console.log(j);
},i*1000,i);
} // 依次输出0~5,间隔为1秒
10. 你真的了解Javascript中的 && 操作符吗?
- 如果第一个操作数是对象,则返回第二个操作数;
- 如果第二个操作数是对象,则只有在第一个操作数的求值结果为 true 的情况下才会返回该 对象;
- 如果两个操作数都是对象,则返回第二个操作数;
- 如果有一个操作数是 null,则返回 null;
- 如果有一个操作数是 NaN,则返回 NaN;
- 如果有一个操作数是 undefined,则返回 undefined。
妙用:可以看我的这篇《理解:有条件的对象属性》
11. 你真的了解Javascript中的 + 操作符吗?
undefined + undefined; // NaN (Not a Number)
undefined + null; // NaN
1 + null; // 1 说明:Number(null) 返回 0 isNaN(null) === false
1 + undefined; // NaN 说明:Number(undefined) 返回 NaN isNaN(undefined) === true
'1' + null; // "1null"
'1' + undefined; // "1undefined"
'1' + 1; // "11"
1 + [1] // "11" 说明:[1].toString() === "1"
1+ {a:1} // "1[object Object]"
在《javaScript高级程序设计》一书"加性操作符"一节中,有很详细的说明,这里摘要三点:
- 如果有一个操作数是 NaN,则结果是 NaN;
- 如果只有一个操作数是字符串,则将另一个操作数转换为字符串,然后再将两个字符串拼接 起来。
不过,如果有一个操作数是字符串,那么就要应用如下规则:
- 如果两个操作数都是字符串,则将第二个操作数与第一个操作数拼接起来;
- 如果只有一个操作数是字符串,则将另一个操作数转换为字符串,然后再将两个字符串拼接 起来。
- 如果有一个操作数是对象、数值或布尔值,则调用它们的 toString()方法取得相应的字符串值, 然后再应用前面关于字符串的规则。对于 undefined 和 null,则分别调用 String()函数并取得字符串"undefined"和"null"。
12. 你知道 == 与 === ,那你知道 Object.is() 与它们的区别吗?
- 相等运算符(==)会自动转换数据类型
- 严格相等运算符(===)NaN 不等于自身,以及+0等于-0
眼见为实:
1 == '1'; // true
1 === '1'; // false
NaN == NaN; // false
NaN === NaN; // false
+0 == -0; // true
+0 === -0; // true
null == undefined; // true
null === undefined; // false
Object.is() 与之不同之处:
- +0不等于-0
- NaN 等于自身
Object.is(+0, -0); // false
Object.is(NaN, NaN); // true
13. Object.create(null) 与 {} 区别
先看 Object.create() 的说明:
Object.create(proto[, propertiesObject])
参数:
proto 新创建对象的原型对象 propertiesObject 可选。如果没有指定为 undefined,则是要添加到新创建对象的不可枚举(默认)属性(即其自身定义的属性,而不是其原型链上的枚举属性)对象的属性描述符以及相应的属性名称。这些属性对应Object.defineProperties()的第二个参数
返回值:
一个新对象,带着指定的原型对象和属性。
Object.create(null) 将返回的新对象的原型设为 null ,相比 {} ,它不会有 Object 对象原型上的如 toString() 等方法,它作为空对象更干净更安全,很多时候用处很大。我喜欢用穷徒四壁来形容 {} ,那么 Object.create(null) 对比之下就是连四壁也没有。
14. ES6 属性的遍历及遍历规则
- for...in
for...in循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)。
- Object.keys(obj)
Object.keys返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名。
- Object.getOwnPropertyNames(obj)
Object.getOwnPropertyNames返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。
- Object.getOwnPropertySymbols(obj)
Object.getOwnPropertySymbols返回一个数组,包含对象自身的所有 Symbol 属性的键名。
- Reflect.ownKeys(obj)
Reflect.ownKeys返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。
遍历规则:
首先遍历所有数值键,按照数值升序排列。
其次遍历所有字符串键,按照加入时间升序排列。
最后遍历所有 Symbol 键,按照加入时间升序排列。
eg:
Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
// ['2', '10', 'b', 'a', Symbol()]
15. 检测对象是否具有某个属性(你所忽略的 in 操作符)
共有3种方式:
if('key' in obj)
会检测继承属性obj.hasOwnProperty('key')
只检测自身属性if(obj.key)
因为原型链机制,会检测继承属性(对于if
中发生的隐式转换请阅读标题16的内容)
注意: 慎用第三种方式,有时这种方式可能达不到你预期的效果。
举例说明:
var myObject = {
a: undefined
};
myObject.a; // undefined
myObject.b; // undefined
16. if 中隐式的类型转换 Boolean()
数据类型 | 转换为true的值 | 转换为false的值 |
---|---|---|
Boolean | true | false |
String | 任何非空字符串 | ""(空字符串) |
Number | 任何非零数字值(包括无穷大) | 0和NaN |
Object | 任何对象 | null |
Undefined | 不适用 | undefined |
17. 对于内置函数 Date() 需要注意的地方
new Date().getDay()
返回的是一周中的某一天new Date().getDate()
返回的是一月中的某一天
tips: 开发中总见有人对此混淆
18. 实现对象不变形的三种方式及其区别
- Object.preventExtensions() 禁止扩展
禁止一个对象添加新属性并且保留已有属性,已有属性可以删除
- Object.seal() 密封
这个方法实际上会在一个现有对象上调用 Object.preventExtensions() 并把所有现有属性标记为 configurable:false。
所以,密封之后不仅不能添加新属性,也不能重新配置或者删除任何现有属性(虽然可以 修改属性的值)。
- Object.freeze() 冻结
这个方法实际上会在一个现有对象上调用 Object.seal() 并把所有“数据访问”属性标记为 writable:false,这样就无法修改它们的值。
这个方法是你可以应用在对象上的级别最高的不可变性,它会禁止对于对象本身及其任意 直接属性的修改
当然对应的还有解除不变性的方法,这里就不介绍了。
19. 浅拷贝与深拷贝
浅拷贝和深拷贝都是相对于Object
, Array
这样的引用类型而言的,因为对基本类型来说没有意义
引用类型数据在内存中的存储 | ||
---|---|---|
栈内存 | 堆内存 | |
name | val | val |
obj | 堆地址(指向堆内存的值) | {a: 1,b: 2...} |
很简单的理解,浅拷贝就是只拷贝了堆地址,而深拷贝是将堆内存中的值拷贝了一份。所以浅拷贝后,修改任何一个变量的值,都会在另一个变量上得到反映,是会相互影响的,深拷贝则不会。
实现浅拷贝的方式:
- 常见的赋值操作
var a = [1, 2, 3];
var b = a;
b[2] = 4;
console.log(a[2]); // 4
- Object.assign()
var obj = {
a: 1,
b: 2
}
var obj1 = Object.assign(obj);
obj1.a = 3;
console.log(obj.a) // 3
实现深拷贝的方式(排除第三方库):
- 利用 JSON 序列化
var obj = {
a: 1,
b: 2
}
var obj1 = JSON.parse(JSON.stringify(obj));
obj1.a = 3;
console.log(obj.a); // 1
- 扩展运算符
var arr = [1, 2, 3];
var arr1 = [...arr];
arr1[2] = 4;
console.log(arr[2]); // 3
- 对于数组的深拷贝,除扩展运算符外,还可以利用数组自身的方法
slice()、concat()
说明:
slice()、concat()
都会返回一个新的数组副本
- 对于深层嵌套的复杂对象,采用递归赋值方式
function deepCopy(a, b= {}) {
for(let item in a){
if(typeof a[item] === "object"){
b[item] = {};
deepCopy(a[item], b[item]);
}else{
b[item] = a[item];
}
}
return b;
}
var a = {
x: 1,
y: {
z: 2
}
}
var b = deepCopy(a);
a.y.z = 4;
console.log(b.y.z); //2 b中属性值并没有改变,说明是深拷贝
20. 参数按 "值" 传递
解释:传入的参数,无论它是基本类型还是引用类型,把握一个原则,按 "值" 传递。理解就是把函数外部值拷贝一份,然后把拷贝的值赋值给内部参数,这里的拷贝特指浅拷贝,所以这里拷贝的值分两种情况:
- 基本类型参数:拷贝的值为基本类型
- 引用类型参数:拷贝的值为内存中的地址引用,可以理解为一个指针指向
举个之前博客的栗子,理解一下:
var type = 'images';
var size = {width: 800, height: 600};
var format = ['jpg', 'png'];
function change(type, size, format){
type = 'video';
size = {width: 1024, height: 768};
format.push('map');
}
change(type, size, format);
console.log(type, size, format); // 'images', {width: 800, height: 600}, ['jpg', 'png', 'map']
21. for 、forEach() 、map() 执行效率
for > forEach() > map()
原因:forEach() 与 map() 遍历需要维护当前项和索引,必然要比 for 慢,而 map() 会分配内存空间存储新数组并返回,forEach() 不会返回数据,所以 map() 最慢 。
22. super 关键字除了用在继承时 class 类中构造函数里面,还能用在哪里?
- 在继承时,子类构造函数中的 super() 代表调用父类的构造函数,返回子类实例。super() 内部的 this 指的是子类的实例,因此 super() 在子类构造函数中相当于
parentClass.prototype.constructor.call(this)。
- super 作为对象时,在普通方法中,指向父类的原型对象
class A {}
A.prototype.x = 2;
class B extends A {
constructor() {
super();
}
print(){
console.log(super.x);
}
}
var b = new B();
b.print(); // 2
- 在静态方法中,指向父类
class A { // 阮老师的例子
constructor() {
this.x = 1;
}
static print() {
console.log(this.x);
}
}
class B extends A {
constructor() {
super();
this.x = 2;
}
static m() {
super.print();
// 在子类的静态方法中通过super调用父类的方法时,方法内部的this指向当前的子类,而不是子类的实例
}
}
B.x = 3;
B.m() // 3
23. 对象与数组的相互转换
善用Object.fromEntries()
与Object.entries()
方法。
Object.fromEntries([
['x', 1],
['y', 2]
])
// { x: 1, y: 2 }
var arr = Object.entries({x: 1, y: 2});
console.log(arr); // [['x', 1], ['y', 2]]
将对象转为数组后,你还可以使用数组的flat
函数将它拉平:
var arr = [['x', 1], ['y', 2]];
arr = arr.flat();
console.log(arr); // ["x", 1, "y", 2]
这里再提供一个将一维数组转换为任意维数组的方法:
function translate(num, arr) { // num:你要转换的维数,arr:初始数组
const newArr = [];
while(arr.length > 0) {
newArr.push(arr.splice(0, num));
}
return newArr;
}
translate(2, [1, 2, 3, 4]); // [[1, 2],[3, 4]]
24. 关于 JSON.stringify()
对大多数简单值来说,JSON
字符串化和 toString()
的效果基本相同,只不过序列化的结
果总是字符串:
JSON.stringify( 42 ); // "42"
JSON.stringify( "42" ); // ""42"" (含有双引号的字符串)
JSON.stringify( null ); // "null"
JSON.stringify( true ); // "true"
JSON.stringify()
在对象中遇到 undefined、function
和 symbol
时会自动将其忽略,在数组中则会返回 null
(以保证单元位置不变)。
JSON.stringify( undefined ); // undefined
JSON.stringify( function(){} ); // undefined
JSON.stringify(
[1,undefined,function(){},4]
); // "[1,null,null,4]"
JSON.stringify(
{ a:2, b:function(){} }
); // "{"a":2}"
对包含循环引用的对象执行 JSON.stringify()
会出错。
如果对象中定义了 toJSON()
方法,JSON
字符串化时会首先调用该方法,然后用它的返回
值来进行序列化。
JSON.stringify()
第二可选参数 replacer
,它可以是数组(必须是一个字符串数组)或者函数(函数有两个参数,键和值),用来指定对象序列化过程中哪些属性应该被处理,哪些应该被排除。
var a = {
b: 42,
c: "42",
d: [1,2,3]
};
JSON.stringify( a, ["b","c"] ); // "{"b":42,"c":"42"}"
JSON.stringify( a, function(k,v){
if (k !== "c") return v;
} );
// "{"b":42,"d":[1,2,3]}"
第三可选参数 space
,用来指定输出的缩进格式。space
为正整数时是指定
每一级缩进的字符数,它还可以是字符串,
JSON.stringify( a, null, "-----" );
// "{
// -----"b": 42,
// -----"c": "42",
// -----"d": [
// ----------1,
// ----------2,
// ----------3
// -----]
// }
25. 关于字符串与数组公有方法 includes
的注意点
字符串的 includes
方法在处理数字时表现出细微差异:
[1, 2, 3].includes('1'); // false
['1', '2', '3'].includes(1); // false
'123'.includes('1'); // true
'123'.includes(1); // true
看出来了吧。另外一个注意点,数组的 includes
方法内部的比较方法既不是严格相等(===
)也不是 Object.is()
,你可以认为是在严格相等(===
)的基础上多一个 NaN
等于 NaN
条件,或者是在 Object.is()
的基础上多一个正零与负零相等的条件。看例子:
[+0, NaN].includes(-0); // true
[+0, NaN].includes(NaN); // true
// -------帮你回忆下严格相等与 Object.is()------
+0 === -0; // true
NaN === NaN; // false
Object.is(+0, -0); // false
Object.is(NaN, NaN); // true
26. 按位与&
的妙用
如果让你判断一个数是否为2
的n
次幂,你会怎么做?考虑一下位操作,可以将其与自身减一相与实现。
var number = 16
(number & number -1) === 0 // true
原理:只是由规律得出来的结论。观察下面的数字,我们发现二进制数值在相对位置总是互为相反的数字,进行&
操作结果必为0
。
十进制 | 二进制 |
---|---|
8 | 1000 |
7 | 0111 |
16 | 10000 |
15 | 01111 |
27. 交换两个变量的值
ES6
下借用解构赋值
可以很简单实现[y, x] = [x, y]
。在ES5
时我们通常的做法是使用中间变量,其实除了这种做法,还有一种做法:
var a = 4,b = 3
a = a ^ b // 7
b = a ^ b // 4
a = a ^ b // 3
这种做法了解一下就可以了,只是为了展示下异或^
操作符的妙用。
28. 取整
Math.floor()
向下取整Math.ceil()
向上取整toFixed(0)
四舍五入parseInt()
只截取整数位,遇到无法解析的情况会返回NaN
~~
截取整数位,不存在返回NaN
的情况
~~'1.2你好' // 0
~~'1.2' // 1
~~'1.7' // 1
~~null // 0
~~undefined // 0
~~NaN // 0
~按位非
操作符的本质是返回数值的反码,谨记~x === -x-1
:
var num1 = 25; // 二进制 00000000000000000000000000011001
var num2 = ~num1; // 二进制 11111111111111111111111111100110
console.log(num2); // -26
29.千字分隔符
之前碰见这种需求都是将字符串反转,然后每间隔3
位插逗号,最后再反转回来。
eg: 1234567890 => 0987654321 => 098,765,432,1 => 1,234,567,890
上面需要将字符串转为数组然后反转遍历插值再反转,很麻烦。而我利用正则一行就搞定:
"1234567890".replace(/\B(?=(?:\d{3})+(?!\d))/g,",") // 结果:"1,234,567,890"
还有一种不太推荐的做法,使用toLocaleString
:
NumberObject.toLocaleString()
返回值:数字的字符串表示,由实现决定,根据本地规范进行格式化,可能影响到小数点或千分位分隔符采用的标点符号。
1234567890..toLocaleString() // 结果:"1,234,567,890"
30. call 与 apply
非严格模式下,call
、apply
的第一参数:
- 为
null
或undefined
时会自动替换为指向全局对象 - 为原始值时会被包装
function a () {
console.log(this, 'a');
}
a.call('b'); // String {"b"} "a"
严格模式:
'use strict'
function a () {
console.log(this, 'a');
}
a.call('b'); // "b" "a"
匪夷所思:
function a () {
console.log(this, 'a')
};
function b () {
console.log(this, 'b')
}
a.call.call(b, 'b') // String {"b"} "b"
a.call.call.call(b, 'b') // String {"b"} "b"
解惑:
call
指向Function.prototype.call
:
a.call === Function.protype.call // true
a.call === a.call.call // true
a.call === a.call.call.call // true
所以a.call.call(b,'b')、a.call.call.call(b,'b')、a.call.call.call.call(b,'b')
等等, 实际是都等同于Function.prototype.call.call(b,'b')
,用f
来代替Function.prototype.call
,即f.call(b,'b')
, 总所周知,call
实际上就是改变f
函数的this
指向,其实际上执行的是b.f('b')
而f
就是call
函数,所以,b.f('b')
就等价于 b.call('b')
,所以结果就是:String {"b"} "b"
,跟a
没有任何关系。
31. 123["toString"].length
123["toString"].length; // 1
答案为1
,看MDN
说话:
对于123["toString"]
拿到Number.prototypr.toString
这个函数,而这个函数的形参为1
,故123["toString"].length
等于1
。
32. 函数的 length
函数的 length
属性表示形参的个数:
function fn1 () {}
function fn2 (name) {}
function fn3 (name, age) {}
console.log(fn1.length) // 0
console.log(fn2.length) // 1
console.log(fn3.length) // 2
可以看出,function
有多少个形参,length
就是多少。
但是如果有默认参数的话,函数的length
将会是 第一个具有默认值之前的参数个数:
function fn1 (name) {}
function fn2 (name = '林三心') {}
function fn3 (name, age = 22) {}
function fn4 (name, age = 22, gender) {}
function fn5(name = '林三心', age, gender) { }
console.log(fn1.length) // 1
console.log(fn2.length) // 0
console.log(fn3.length) // 1
console.log(fn4.length) // 1
console.log(fn5.length) // 0
在函数的形参中,如果具有剩余参数
,是不会被算进length
的计算之中的:
function fn1(name, ...args) {}
console.log(fn1.length) // 1
33. 你真的懂变量提升吗?
console.log(foo);
function foo(){
console.log("函数声明");
}
var foo = "变量";
输出结果为:
function foo(){
console.log("函数声明");
}
上述代码等价于:
function foo(){
console.log("函数声明");
}
var foo;
console.log(foo);
foo = "变量";
总结:编译阶段,js引擎会去扫描所有声明变量/函数标识符,当遇到声明的变量标识符,提升并赋值为undefined
,当遇到用function
关键字声明的函数标识符,提升并赋值为函数对象的引用。js引擎不会去判断谁优先级高,在第一次声明后,之后相同名字标识符的声明会就被忽略。
注意:讲的是声明被忽略,函数声明可以被忽略,但函数的赋值不会被忽略,这就是为什么很多人错觉“函数提升优先级比变量提升高”,实际上是被在编译阶段被赋值为函数引用而已。
结语
基础是需要不断巩固的,温故而知新。