面试题

166 阅读13分钟

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。

别怕,我当时也是,但是认真分析一下,还是可以懂的。

解析: (认真理解下面这段解析)

上面代码中,变量ivar命令声明的,在全局范围内都有效,所以全局只有一个变量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构造器定义函数在每次构造器被调用的时候,构造器都会被解析一次,因为字符串的函数体需要每次都需要重新进行解析,不像其他方式那样,只需要解析其他的代码就可以了。

这种方式定义函数的一种用途是在浏览器中通过Nodewindow对象访问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 版权协议,转载请附上原文出处链接和本声明。

原文链接:blog.csdn.net/l_ppp/artic…