let、const、var的区别?
1.块级作用域:(必答)
块级作用域是: 由{}包括起来的部分
具有块级作用域: let和const;
不具有块级作用域: var
⭐补充: (亮点)
块级作用域解决了ES5中的两个问题:
① 内层变量可能覆盖外层变量
② 用来计数的循环变量泄露为全局变量
ok,我将用例子来说一下上面这两个问题!
<script>
var x = 10;//外层变量x
if (true) {
var x = 20;//内层变量x
console.log(x);
}
console.log(x);//这里本来应该是输出10,但是因为内层变量x的原因,所以外层变量x的值被内层变量x的值覆盖了
</script>
//浏览器输出的结果是:20 20
如果把上面这段代码换成let来声明,会怎么样?
let x = 10;
if (true) {
let x = 20;
console.log(x);//输出20
}
console.log(x);//输出10,为什么不是20呢?因为let x = 20是在块里面,它不会覆盖外层let x = 10 的值,所以输出就是10
ok,到此,解决了第一个问题“① 内层变量可能覆盖外层变量”!
接下来,解决第二个问题!
直接上代码解释:
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[5]();//10
a[6](); // 10
可能大家看到这个,一时懵了,为什么都是10。
别怕,我当时也是,但是认真分析一下,还是可以懂的。
解析: (认真理解下面这段解析)
上面代码中,变量i是var命令声明的,在全局范围内都有效,所以全局只有一个变量i。
每一次循环,变量i的值都会发生改变,而循环内被赋给数组a的函数内部的console.log(i),里面的i指向的就是全局的i。
也就是说,所有数组a的成员里面的i,指向的都是同一个i,导致运行时输出的是最后一轮的i的值,也就是 10。
ok,那如果我把上面对i的声明用let声明,那结果会是怎么样呢?
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[5]();//5
a[6]();//6
现在就不是10了,因为跟let是块级作用域有很大关系,那么同样解析一下:
上面代码中,变量i是let声明的,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量,所以最后输出的是5、6。
你可能会问,如果每一轮循环的变量i都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。
2.变量提升: (必答)
存在变量提升: var;
不存在变量提升: let和const;
即变量只能在声明之后使用,否则报错❌
// var 的情况
console.log(foo); // 输出undefined
var foo = 2;
//上面代码中,变量foo用var命令声明,会发生变量提升
//即脚本开始运行时,变量foo就已经存在了,但是没有值,所以会输出undefined。
//上面代码运行时,是下面这样子的:
var foo;
console.log(foo);
// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;
//上面第17、18行代码中,变量bar用let声明,不会发生变量提升。
//这表示在声明它之前,变量bar是不存在的,这时如果用到它,就会抛出一个错误
3.给全局添加属性: (感觉不用非得答)
会将变量添加为全局对象的属性的是:var;
不会将变量添加为全局对象的属性的是:let和const;
浏览器的全局对象是window,node的全局对象是global,那var呢?var声明的变量为全局变量,且会将变量添加为全局对象的属性。
4.重复声明: (必答)
var 声明变量时,可以重复声明变量,后声明的同名变量会覆盖之前声明的变量;
let和const不允许重复声明变量
5.暂时性死区: (必答)
在使用let、const命令声明变量之前,该变量都是不可用的,这在语法上,称为“暂时性死区”。
使用var声明的变量不存在暂时性死区。
6.初始值设置: (必答)
在声明变量的时候,var 和 let 可以不用设置初始值,而const必须要设置初始值
7.指针指向: (必答)
let创建的变量是可以更改指针指向(也就是说可以重新赋值),var也可以,但是const不可以
作者:沉浸学习的匿名网友
链接:juejin.cn/post/726853…
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
JavaScript中定义函数的方法
一般来说,函数是一串指令或是一段可以被该函数内部或外部代码调用的“子程序”,本质上,函数“封装”了一个特定的任务。函数是JavaScript中一个基础模块,真正理解函数可以帮助理解JavaScript的一些奇怪的点。
JavaScript中的函数
需要注意的是,函数是JavaScript的重要的一类对象,这意味着JavaScript中的函数可以像其他对象一样进行处理,并且像其他变量一样被引用或作为参数传入函数中。
函数甚至有属性和方法,就像其他JavaScript对象一样。函数和其他对象的关键区别在于函数可以调用。
JavaScript中的每个函数是个Function对象,可以进到控制台然后尝试以下脚本:
function typeCheck() {};
typeCheck instanceof Function // Logs True
Function对象有一些内置的方法和属性,如apply, call, bind, isGenerator 等等,而其他对象就没有。
在JavaScript中定义函数有好几种方式,不同的定义方式会影响函数最后的行为,下面就挨个看一下。
函数声明
这是最常见的定义函数的方式了,函数声明包括关键字function,其后是一个函数名,接着是一对必需要有的括号,括号内是可选的参数列表。
function sum(param1, param2) {
return param1 + param2;
}
用这种方式定义函数有两件事需要注意一下:
- 在当前作用域下会创建一个包含这个函数对象的变量,变量的标识名就是函数的名称,在本例中指的就是
sum。 - 变量是会被提升到当前作用域的顶层域的。可以在这里查看更多内容 为了更好的理解提升,看个例子:
console.log(notYetDeclared()); // Logs 'Here!'
function notYetDeclared() {
return 'Here';
}
我们可以在定义notYetDeclared之前就调用这个函数。
函数表达式
函数表达式在语法上和函数声明很相似,主要的不同在于函数表达式不需要函数名。
let sum = function(param1, param2) {
return param1 + param2;
};
函数表达式是一条语句的一部分,上面的例子中,函数表达式就是sum变量赋值的一部分, 不同于函数声明的是函数表达式不会进行提升。
console.log(notYetDeclared); // Logs 'undefined'
let notYetDeclared = function() {
return 'Here';
}
函数表达式的一个有意思的使用场景是创建IIFE或是立即调用函数表达式的能力,很多时候我们想定义一个函数然后立即调用它,但是其他地方不会再调用。
当然,使用函数声明也可以做到,但为了让代码可读性更好,并且让其他程序不会意外访问到它,可以使用IIFE,看下面的代码:
function callImmediately(foo) {
console.log(foo);
}
callImmediately('foo'); // Logs 'foo'
上面的代码创建了callImediately函数,该函数包含参数foo,并把参数foo打印到控制台,然后立即调用它。下面这样做可以达到同样的结果:
(function(foo) {
console.log(foo);
})('foo'); // Logs 'foo'
两者关键的不同在于第一个例子中函数声明污染了全局命名空间,并且命名函数callImmediately在使用后很长一段时间依然随处可调用。但IIFE是匿名的,因此以后是不能调用的。
箭头函数
箭头函数是ES6新增的,是函数表达式的语法紧凑版,箭头函数通过使用一对包含参数列表的括号进行定义,跟着一个双箭头=>,然后是由大括号{}包裹函数语句。
let sum = (param1, param2) => {
return param1 + param2;
};
由于箭头函数背后的主要动机之一是语法紧凑,如果箭头函数中只有return语句,可以去掉大括号和return关键字,像下面这样:
let sum = (param1, param2) => param1 + param2;
同样,如果只有一个参数,大括号也可以去掉:
let double = param1 => param1 * 2;
这种函数定义的形式需要注意的是:
- 箭头函数没有自己的
this,它使用的是封闭词法作用域,可以在这里阅读更多关于this的内容
let foo = {
id: 10,
logIdArrow: () => { console.log(this.id) },
logIdExpression: function() {
console.log(this.id);
}
}
foo.logIdArrow(); // Logs 'undefined'
foo.logIdExpression(); // Logs '10'
上面的例子中,foo对象中定义了一个箭头函数和一个函数表达式,两者都调用this记录了foo.id。
- 箭头函数没有
prototype属性。
let foo = () => {};
console.log(foo.prototype); // Logs 'undefined'
arguments对象在箭头函数内是不可用的,可以在这里阅读更多关于arguments对象的内容
Function 构造器
前面提到过,JavaScript中每个函数是Function对象,所以要定义函数,可以直接调用Function对象的构造器。
let sum = new Function('param1', 'param2', 'return param1 + param2');
参数以逗号分隔的字符串'param1', 'param2', 'param3', ..., 'paramN'传入,函数体作为最后一个参数传入。
从性能方面考虑的话,这种定义函数的方式没有函数声明或函数表达式那么高效,使用Function构造器定义函数在每次构造器被调用的时候,构造器都会被解析一次,因为字符串的函数体需要每次都需要重新进行解析,不像其他方式那样,只需要解析其他的代码就可以了。
这种方式定义函数的一种用途是在浏览器中通过Node或window对象访问global对象。这些函数经常在全局作用域中创建并且不需要在当前作用域中访问。
Generator函数
Generator函数是ES6新增的。Generator函数是一种特殊的函数类型,就意义上而言它不像传统的函数,Generator函数会在每个请求的基础上生成多个值,并在这些请求之间暂停它们的执行。
function* idMaker() {
let index = 0;
while(true)
yield index++;
}
let gen = idMaker();
console.log(gen.next().value); // Logs 0
console.log(gen.next().value); // Logs 1
console.log(gen.next().value); // Logs 2
function*和yield关键字对于generator函数来说是唯一的。Generator函数通过在function尾部添加一个 * 来定义Generator函数,这让我们在generator函数体中可以用yield在请求上生成多个值。可以在这里查看更多的细节。
总结
选用哪种定义类型取决于实际情况和你想要实现什么。以下是几个可以考虑的点:
-
如果想利用函数提升,使用函数声明,例如,为了清晰起见顶部仅仅是抽象流,而将函数实现细节放在底部。
-
箭头函数同样适用于短回调函数,更重要的是使用this时是封闭函数。
-
尽量避免使用
Function构造器来定义函数,如果讨厌的语法不足以让你远离他,那么你需要了解它的速度非常慢的,每次调用时都会解析一次。
作者:道奇
链接:juejin.cn/post/684490…
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
操作数组的方法
添加和删除元素
push(): 在数组末尾添加一个或多个元素,并返回新数组的长度。
var arr = [1, 2, 3];
arr.push(4); // [1, 2, 3, 4]
unshift(): 在数组开头添加一个或多个元素,并返回新数组的长度。
var arr = [1, 2, 3];
arr.unshift(0); // [0, 1, 2, 3]
pop(): 删除数组的最后一个元素,并返回该元素。
var arr = [1, 2, 3];
arr.pop(); // [1, 2]
shift(): 删除数组的第一个元素,并返回该元素。
var arr = [1, 2, 3];
arr.shift(); // [2, 3]
修改和查询元素
splice(): 在指定位置删除或添加元素。
var arr = [1, 2, 3, 4];
arr.splice(1, 2, 'a', 'b'); // [1, 'a', 'b', 4]
concat(): 合并两个或多个数组,不改变原数组。
var arr1 = [1, 2];
var arr2 = [3, 4]; var newArr = arr1.concat(arr2); // [1, 2, 3, 4]
slice(): 返回数组的一个子数组,不改变原数组。
var arr = [1, 2, 3, 4];
var subArr = arr.slice(1, 3); // [2, 3]
indexOf(): 返回元素在数组中第一次出现的位置。
var arr = [1, 2, 3, 4];
var index = arr.indexOf(3); // 2
lastIndexOf(): 返回元素在数组中最后一次出现的位置。
var arr = [1, 2, 3, 4, 3];
var index = arr.lastIndexOf(3); // 4
遍历和过滤元素
forEach(): 遍历数组,执行回调函数。
var arr = [1, 2, 3];
arr.forEach(function(item) { console.log(item); });
map(): 遍历数组,生成一个新数组。
var arr = [1, 2, 3];
var newArr = arr.map(function(item) { return item * 2; });
// [2, 4, 6]
filter(): 过滤数组,返回满足条件的元素组成的新数组。
var arr = [1, 2, 3, 4];
var newArr = arr.filter(function(item) { return item > 2; });
// [3, 4]
reduce(): 累加数组中的元素,返回最终结果。
var arr = [1, 2, 3, 4];
var sum = arr.reduce(function(prev, curr) { return prev + curr; }, 0);
// 10
判断一个变量是否为数组类型
instanceof
使用 instanceof 运算符, 该运算符左边是我们想要判断的变量, 右边则是我们想要判断的对象的类, 例如:
let arr = [1, 2, 3]
console.log(arr instanceof Array)
// true 返回true,说明变量arr是数组类型
构造函数constructor
利用构造函数来判断他的原型是否为Array, 用法: 变量.constructor === 变量类型
let arr = [1, 2, 3]
console.log(arr.constructor === Array)
// true 返回true,说明变量arr是数组类型
方法三 第三种方法利用的一个专门的方法 isArray(), 用法:Array.isArray(变量),返回true,则说明该变量是数组类型;反之,说明该变量不是数组类型
let arr = [1, 2, 3]
console.log(Array.isArray(arr))
// true 返回true,说明变量arr是数组类型
Object.prototype.toString.call()
第四种方法是调用Object.prototype.toString.call(),返回true,则说明该变量是数组类型;反之,说明该变量不是数组类型
let arr = [1, 2, 3]
console.log(Object.prototype.toString.call(arr) === '[object Array]')
// true 返回true,说明变量arr是数组类型
对象的原型方式
第五种方式是通过对象的原型方式来判断,直接来看例子
let arr = [1, 2, 3]
console.log(arr.__proto__ === Array.prototype)
// true 返回true,说明变量arr是数组类型
Object.getPrototypeOf()
第六种方式是通过Object.getPrototypeOf() 来判断是否为数组类型,例如
let arr = [1, 2, 3]
console.log(Object.getPrototypeOf(arr) === Array.prototype)
// true 返回true,说明变量arr是数组类型
isPrototypeOf()
第七种方式是通过 isPrototypeOf() 方法来判断是否为数组类型,例如
let arr = [1, 2, 3]
console.log(Array.prototype.isPrototypeOf(arr))
// true 返回true,说明变量arr是数组类型
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。