20道JavaScript经典面试题
该篇文章整理了一些前端经典面试题,附带详解,涉及到JavaScript多方面知识点,满满都是干货~建议收藏阅读
前言
如果这篇文章有帮助到你,❤️关注+点赞❤️鼓励一下作者,文章公众号首发,关注 前端南玖 第一时间获取最新的文章~
1.说一说JavaScript的数据类型以及存储方式
JavaScript一共有8种数据类型
其中有7种基本数据类型:
ES5的5种:Null
,undefined
,Boolean
,Number
,String
,
ES6新增:Symbol
表示独一无二的值
ES10新增:BigInt
表示任意大的整数
一种引用数据类型:
Object
(本质上是由一组无序的键值对组成)
包含function
,Array
,Date
等。JavaScript不支持创建任何自定义类型的数据,也就是说JavaScript中所有值的类型都是上面8中之一。
存储方式
- 基本数据类型:直接存储在栈内存中,占据空间小,大小固定,属于被频繁使用的数据。
- 引用数据类型:同时存储在栈内存与堆内存中,占据空间大,大小不固定。引用数据类型将指针存在栈中,将值存在堆中。当我们把对象值赋值给另外一个变量时,复制的是对象的指针,指向同一块内存地址。
null 与 undefined的异同
相同点:
- Undefined 和 Null 都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 undefined 和 null
不同点:
-
undefined 代表的含义是未定义, null 代表的含义是空对象。
-
typeof null 返回'object',typeof undefined 返回'undefined'
-
null == undefined // true null === undefined // false
-
其实 null 不是对象,虽然 typeof null 会输出 object,但是这只是 JS 存在的一个悠久 Bug。在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象,然而 null 表示为全零,所以将它错误的判断为 object 。虽然现在的内部类型判断代码已经改变了,但是对于这个 Bug 却是一直流传下来。
2.说说JavaScript中判断数据类型的几种方法
typeof
typeof
一般用来判断基本数据类型,除了判断null会输出"object",其它都是正确的typeof
判断引用数据类型时,除了判断函数会输出"function",其它都是输出"object"
console.log(typeof 6); // 'number'
console.log(typeof true); // 'boolean'
console.log(typeof 'nanjiu'); // 'string'
console.log(typeof []); // 'object' []数组的数据类型在 typeof 中被解释为 object
console.log(typeof function(){}); // 'function'
console.log(typeof {}); // 'object'
console.log(typeof undefined); // 'undefined'
console.log(typeof null); // 'object' null 的数据类型被 typeof 解释为 object
对于引用数据类型的判断,使用typeof
并不准确,所以可以使用instanceof
来判断引用数据类型
instanceof
Instanceof 可以准确的判断引用数据类型,它的原理是检测构造函数的
prototype
属性是否在某个实例对象的原型链上
原型知识点具体可以看我之前的文章:你一定要懂的JavaScript之原型与原型链
语法:
object instanceof constructor
console.log(6 instanceof Number); // false
console.log(true instanceof Boolean); // false
console.log('nanjiu' instanceof String); // false
console.log([] instanceof Array); // true
console.log(function(){} instanceof Function); // true
console.log({} instanceof Object); // true
constructor(构造函数)
当一个函数被定义时,JS引擎会为函数添加prototype
属性,然后在prototype
属性上添加一个constructor
属性,并让其指向该函数。
当执行 let f = new F()
时,F被当成了构造函数,f是F的实例对象,此时F原型上的constructor属性传递到了f上,所以f.constructor===F
function F(){}
let f = new F()
f.constructor === F // true
new Number(1).constructor === Number //true
new Function().constructor === Function // true
true.constructor === Boolean //true
''.constructor === String // true
new Date().constructor === Date // true
[].constructor === Array
⚠️注意:
- null和undefined是无效的对象,所以他们不会有constructor属性
- 函数的construct是不稳定的,主要是因为开发者可以重写prototype,原有的construction引用会丢失,constructor会默认为Object
function F(){}
F.prototype = {}
let f = new F()
f.constructor === F // false
console.log(f.constructor) //function Object(){..}
为什么会变成Object?
因为prototype
被重新赋值的是一个{}
,{}
是new Object()
的字面量,因此 new Object()
会将 Object 原型上的 constructor
传递给 { },也就是 Object 本身。
因此,为了规范开发,在重写对象原型时一般都需要重新给 constructor 赋值,以保证对象实例的类型不被篡改。
Object.prototype.toString.call()
toString() 是 Object 的原型方法,调用该方法,默认返回当前对象的 [[Class]] 。这是一个内部属性,其格式为 [object Xxx] ,其中 Xxx 就是对象的类型。
对于 Object 对象,直接调用 toString() 就能返回 [object Object] 。而对于其他对象,则需要通过 call / apply 来调用才能返回正确的类型信息。
Object.prototype.toString.call('') ; // [object String]
Object.prototype.toString.call(1) ; // [object Number]
Object.prototype.toString.call(true) ; // [object Boolean]
Object.prototype.toString.call(Symbol()); //[object Symbol]
Object.prototype.toString.call(undefined) ; // [object Undefined]
Object.prototype.toString.call(null) ; // [object Null]
Object.prototype.toString.call(new Function()) ; // [object Function]
Object.prototype.toString.call(new Date()) ; // [object Date]
Object.prototype.toString.call([]) ; // [object Array]
Object.prototype.toString.call(new RegExp()) ; // [object RegExp]
Object.prototype.toString.call(new Error()) ; // [object Error]
Object.prototype.toString.call(document) ; // [object HTMLDocument]
Object.prototype.toString.call(window) ; //[object global] window 是全局对象 global 的引用
3.js数据类型转换
在JavaScript中类型转换有三种情况:
- 转换为数字(调用Number(),parseInt(),parseFloat()方法)
- 转换为字符串(调用.toString()或String()方法)
- 转换为布尔值(调用Boolean()方法)
null、undefined没有.toString方法
转换为数字
- Number():可以把任意值转换成数字,如果要转换的字符串中有不是数字的值,则会返回
NaN
Number('1') // 1
Number(true) // 1
Number('123s') // NaN
Number({}) //NaN
- parseInt(string,radix):解析一个字符串并返回指定基数的十进制整数,radix是2-36之间的整数,表示被解析字符串的基数。
parseInt('2') //2
parseInt('2',10) // 2
parseInt('2',2) // NaN
parseInt('a123') // NaN 如果第一个字符不是数字或者符号就返回NaN
parseInt('123a') // 123
- parseFloat(string):解析一个参数并返回一个浮点数
parseFloat('123a')
//123
parseFloat('123a.01')
//123
parseFloat('123.01')
//123.01
parseFloat('123.01.1')
//123.01
- 隐式转换
let str = '123'
let res = str - 1 //122
str+1 // '1231'
+str+1 // 124
转换为字符串
- .toString() ⚠️注意:null,undefined不能调用
Number(123).toString()
//'123'
[].toString()
//''
true.toString()
//'true'
- String() 都能转
String(123)
//'123'
String(true)
//'true'
String([])
//''
String(null)
//'null'
String(undefined)
//'undefined'
String({})
//'[object Object]'
- 隐式转换:当+两边有一个是字符串,另一个是其它类型时,会先把其它类型转换为字符串再进行字符串拼接,返回字符串
let a = 1
a+'' // '1'
转换为布尔值
0, ''(空字符串), null, undefined, NaN会转成false,其它都是true
- Boolean()
Boolean('') //false
Boolean(0) //false
Boolean(1) //true
Boolean(null) //false
Boolean(undefined) //false
Boolean(NaN) //false
Boolean({}) //true
Boolean([]) //true
- 条件语句
let a
if(a) {
//... //这里a为undefined,会转为false,所以该条件语句内部不会执行
}
- 隐式转换 !!
let str = '111'
console.log(!!str) // true
4.{}和[]的valueOf和toString的返回结果?
- valueOf:返回指定对象的原始值
对象 | 返回值 |
---|---|
Array | 返回数组对象本身。 |
Boolean | 布尔值。 |
Date | 存储的时间是从 1970 年 1 月 1 日午夜开始计的毫秒数 UTC。 |
Function | 函数本身。 |
Number | 数字值。 |
Object | 对象本身。这是默认情况。 |
String | 字符串值。 |
Math 和 Error 对象没有 valueOf 方法。 |
- toString:返回一个表示对象的字符串。默认情况下,
toString()
方法被每个Object
对象继承。如果此方法在自定义对象中未被覆盖,toString()
返回 "[object type]",其中type
是对象的类型。
({}).valueOf() //{}
({}).toString() //'[object Object]'
[].valueOf() //[]
[].toString() //''
5.let,const,var的区别?
- 变量提升:let,const定义的变量不会出现变量提升,而var会
- 块级作用域:let,const 是块作用域,即其在整个大括号 {} 之内可见,var:只有全局作用域和函数作用域概念,没有块级作用域的概念。
- 重复声明:同一作用域下let,const声明的变量不允许重复声明,而var可以
- 暂时性死区:let,const声明的变量不能在声明之前使用,而var可以
- const 声明的是一个只读的常量,不允许修改
6.JavaScript作用域与作用域链
作用域:
简单来说,作用域是指程序中定义变量的区域,它决定了当前执行代码对变量的访问权限
作用域链:
当可执行代码内部访问变量时,会先查找当前作用域下有无该变量,有则立即返回,没有的话则会去父级作用域中查找...一直找到全局作用域。我们把这种作用域的嵌套机制称为
作用域链
详细知识可以看我之前的文章:JavaScript深入之作用域与闭包
7.如何正确的判断this指向?
this的绑定规则有四种:默认绑定,隐式绑定,显式绑定,new绑定.
- 函数是否在 new 中调用(new绑定),如果是,那么 this 绑定的是new中新创建的对象。
- 函数是否通过 call,apply 调用,或者使用了 bind (即硬绑定),如果是,那么this绑定的就是指定的对象。
- 函数是否在某个上下文对象中调用(隐式绑定),如果是的话,this 绑定的是那个上下文对象。一般是 obj.foo()
- 如果以上都不是,那么使用默认绑定。如果在严格模式下,则绑定到 undefined,否则绑定到全局对象。
- 如果把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind, 这些值在调用时会被忽略,实际应用的是默认绑定规则。
- 箭头函数没有自己的 this, 它的this继承于上一层代码块的this。
详细知识可以看我之前的文章:this指向与call,apply,bind
8.for...of,for..in,forEach,map的区别?
for...of(不能遍历对象)
在可迭代对象(具有 iterator 接口)(Array,Map,Set,String,arguments)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句,不能遍历对象
let arr=["前端","南玖","ssss"];
for (let item of arr){
console.log(item)
}
//前端 南玖 ssss
//遍历对象
let person={name:"南玖",age:18,city:"上海"}
for (let item of person){
console.log(item)
}
// 我们发现它是不可以的 我们可以搭配Object.keys使用
for(let item of Object.keys(person)){
console.log(person[item])
}
// 南玖 18 上海
for...in
for...in循环:遍历对象自身的和继承的可枚举的属性, 不能直接获取属性值。可以中断循环。
let person={name:"南玖",age:18,city:"上海"}
let text=""
for (let i in person){
text+=person[i]
}
// 输出:南玖18上海
//其次在尝试一些数组
let arry=[1,2,3,4,5]
for (let i in arry){
console.log(arry[i])
}
//1 2 3 4 5
forEach
forEach: 只能遍历数组,不能中断,没有返回值(或认为返回值是undefined)。
let arr=[1,2,3];
const res = arr.forEach(item=>{
console.log(item*3)
})
// 3 6 9
console.log(res) //undefined
console.log(arr) // [1,2,3]
map
map: 只能遍历数组,不能中断,返回值是修改后的数组。
let arr=[1,2,3];
const res = arr.map(item=>{
return res+1
})
console.log(res) //[2,3,4]
console.log(arr) // [1,2,3]
总结
- forEach 遍历列表值,不能使用 break 语句或使用 return 语句
- for in 遍历对象键值(key),或者数组下标,不推荐循环一个数组
- for of 遍历列表值,允许遍历 Arrays(数组), Strings(字符串), Maps(映射), Sets(集合)等可迭代的数据结构等.在 ES6 中引入的 for of 循环,以替代 for in 和 forEach() ,并支持新的迭代协议。
- for in循环出的是key,for of循环出的是value;
- for of是ES6新引入的特性。修复了ES5的for in的不足;
- for of不能循环普通的对象,需要通过和Object.keys()搭配使用。
9.说说你对原型链的理解?
每个函数(类)天生自带一个属性prototype
,属性值是一个对象,里面存储了当前类供实例
使用的属性和方法 「(显示原型)」
在浏览器默认给原型开辟的堆内存中有一个constructor
属性:存储的是当前类本身(⚠️注意:自己开辟的堆内存中默认没有constructor
属性,需要自己手动添加)「(构造函数)」
每个对象都有一个__proto__
属性,这个属性指向当前实例所属类的原型
(不确定所属类,都指向Object.prototype
)「(隐式原型)」
当你试图获取一个对象的某个属性时,如果这个对象本身没有这个属性,那么它会去它的隐式原型__proto__
(也就是它的构造函数的显示原型prototype
)中查找。「(原型链)」
详细知识可以看我之前的文章:你一定要懂的JavaScript之原型与原型链
10.说一说三种事件模型?
事件模型
DOM0级模型: ,这种模型不会传播,所以没有事件流的概念,但是现在有的浏览器支持以冒泡的方式实现,它可以在网页中直接定义监听函数,也可以通过 js属性来指定监听函数。这种方式是所有浏览器都兼容的。
IE 事件模型: 在该事件模型中,一次事件共有两个过程,事件处理阶段,和事件冒泡阶段。事件处理阶段会首先执行目标元素绑定的监听事件。然后是事件冒泡阶段,冒泡指的是事件从目标元素冒泡到 document,依次检查经过的节点是否绑定了事件监听函数,如果有则执行。这种模型通过 attachEvent 来添加监听函数,可以添加多个监听函数,会按顺序依次执行。
DOM2 级事件模型: 在该事件模型中,一次事件共有三个过程,第一个过程是事件捕获阶段。捕获指的是事件从 document 一直向下传播到目标元素,依次检查经过的节点是否绑定了事件监听函数,如果有则执行。后面两个阶段和 IE 事件模型的两个阶段相同。这种事件模型,事件绑定的函数是 addEventListener,其中第三个参数可以指定事件是否在捕获阶段执行。
事件委托
事件委托指的是把一个元素的事件委托到另外一个元素上。一般来讲,会把一个或者一组元素的事件委托到它的父层或者更外层元素上,真正绑定事件的是外层元素,当事件响应到需要绑定的元素上时,会通过事件冒泡机制从而触发它的外层元素的绑定事件上,然后在外层元素上去执行函数。
事件传播(三个阶段)
- 捕获阶段–事件从 window 开始,然后向下到每个元素,直到到达目标元素事件或event.target。
- 目标阶段–事件已达到目标元素。
- 冒泡阶段–事件从目标元素冒泡,然后上升到每个元素,直到到达 window。
事件捕获
当事件发生在 DOM 元素上时,该事件并不完全发生在那个元素上。在捕获阶段,事件从window开始,一直到触发事件的元素。window----> document----> html----> body ---->目标元素
事件冒泡
事件冒泡刚好与事件捕获相反,当前元素---->body ----> html---->document ---->window
。当事件发生在DOM元素上时,该事件并不完全发生在那个元素上。在冒泡阶段,事件冒泡,或者事件发生在它的父代,祖父母,祖父母的父代,直到到达window为止。
如何阻止事件冒泡
w3c的方法是e.stopPropagation(),IE则是使用e.cancelBubble = true。例如:
window.event?window.event.cancelBubble = true : e.stopPropagation();
return false也可以阻止冒泡。
11.JS延迟加载的方式
JavaScript会阻塞DOM的解析,因此也就会阻塞DOM的加载。所以有时候我们希望延迟JS的加载来提高页面的加载速度。
- 把JS放在页面的最底部
- script标签的defer属性:脚本会立即下载但延迟到整个页面加载完毕再执行。该属性对于内联脚本无作用 (即没有 「src」 属性的脚本)。
- Async是在外部JS加载完成后,浏览器空闲时,Load事件触发前执行,标记为async的脚本并不保证按照指定他们的先后顺序执行,该属性对于内联脚本无作用 (即没有 「src」 属性的脚本)。
- 动态创建script标签,监听dom加载完毕再引入js文件
12.说说什么是模块化开发?
模块化的开发方式可以提高代码复用率,方便进行代码的管理。通常一个文件就是一个模块,有自己的作用域,只向外暴露特定的变量和函数。
几种模块化方案
-
第一种是
CommonJS
方案,它通过require
来引入模块,通过module.exports
定义模块的输出接口。 -
第二种是
AMD
方案,这种方案采用异步加载的方式来加载模块,模块的加载不影响后面语句的执行,所有依赖这个模块的语句都定义在一个回调函数里,等到加载完成后再执行回调函数。require.js 实现了 AMD 规范。 -
第三种是
CMD
方案,这种方案和AMD
方案都是为了解决异步模块加载的问题,sea.js
实现了 CMD 规范。它和require.js
的区别在于模块定义时对依赖的处理不同和对依赖模块的执行时机的处理不同。 -
第四种方案是
ES6
提出的方案,使用import
和export
的形式来导入导出模块。
CommonJS
Node.js
是commonJS
规范的主要践行者。这种模块加载方案是服务器端的解决方案,它是以同步的方式来引入模块的,因为在服务端文件都存储在本地磁盘,所以读取非常快,所以以同步的方式加载没有问题。但如果是在浏览器端,由于模块的加载是使用网络请求,因此使用异步加载的方式更加合适。
// 定义模块a.js
var title = '前端';
function say(name, age) {
console.log(`我是${name},今年${age}岁,欢迎关注我~`);
}
module.exports = { //在这里写上需要向外暴露的函数、变量
say: say,
title: title
}
// 引用自定义的模块时,参数包含路径,可省略.js
var a = require('./a');
a.say('南玖', 18); //我是南玖,今年18岁,欢迎关注我~
AMD与require.js
AMD规范采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。这里介绍用require.js
实现AMD
规范的模块化:用require.config()
指定引用路径等,用define()
定义模块,用require()
加载模块。
CMD与sea.js
CMD是另一种js模块化方案,它与AMD很类似,不同点在于:AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。此规范其实是在sea.js推广过程中产生的。
ES6 Module
ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,旨在成为浏览器和服务器通用的模块解决方案。其模块功能主要由两个命令构成:export
和import
。export
命令用于规定模块的对外接口,import
命令用于输入其他模块提供的功能。
// 定义模块a.js
var title = '前端';
function say(name, age) {
console.log(`我是${name},今年${age}岁,欢迎关注我~`);
}
export { //在这里写上需要向外暴露的函数、变量
say,
title
}
// 引用自定义的模块时,参数包含路径,可省略.js
import {say,title} from "./a"
say('南玖', 18); //我是南玖,今年18岁,欢迎关注我~
CommonJS 与 ES6 Module 的差异
CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
- CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
- ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令
import
,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import
有点像 Unix 系统的“符号连接”,原始值变了,import
加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
- 运行时加载: CommonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。
- 编译时加载: ES6 模块不是对象,而是通过
export
命令显式指定输出的代码,import
时采用静态命令的形式。即在import
时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”。
CommonJS 加载的是一个对象(即module.exports
属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
推荐阅读前端模块化理解
13.说说JS的运行机制
推荐阅读探索JavaScript执行机制
14.如何在JavaScript中比较两个对象?
对于两个非基本类型的数据,我们用==
或===
都指示检查他们的引用是否相等,并不会检查实际引用指向的值是否相等。
例如,默认情况下,数组将被强制转换成字符串,并使用逗号连接所有元素
let a = [1,2,3]
let b = [1,2,3]
let c = "1,2,3"
a == b // false
a == c // true
b == c // true
一般比较两个对象会采用递归来比较
15.说说你对闭包的理解,以及它的原理和应用场景?
一个函数和对其周围(词法环境)的引用捆绑在一起(或者说函数被引用包围),这样一个组合就是闭包(「closure」)
闭包原理
函数执行分成两个阶段(预编译阶段和执行阶段)。
- 在预编译阶段,如果发现内部函数使用了外部函数的变量,则会在内存中创建一个“闭包”对象并保存对应变量值,如果已存在“闭包”,则只需要增加对应属性值即可。
- 执行完后,函数执行上下文会被销毁,函数对“闭包”对象的引用也会被销毁,但其内部函数还持用该“闭包”的引用,所以内部函数可以继续使用“外部函数”中的变量
利用了函数作用域链的特性,一个函数内部定义的函数会将包含外部函数的活动对象添加到它的作用域链中,函数执行完毕,其执行作用域链销毁,但因内部函数的作用域链仍然在引用这个活动对象,所以其活动对象不会被销毁,直到内部函数被烧毁后才被销毁。
优点
- 可以从内部函数访问外部函数的作用域中的变量,且访问到的变量长期驻扎在内存中,可供之后使用
- 避免变量污染全局
- 把变量存到独立的作用域,作为私有成员存在
缺点
- 对内存消耗有负面影响。因内部函数保存了对外部变量的引用,导致无法被垃圾回收,增大内存使用量,所以使用不当会导致内存泄漏
- 对处理速度具有负面影响。闭包的层级决定了引用的外部变量在查找时经过的作用域链长度
- 可能获取到意外的值(captured value)
应用场景
- 模块封装,防止变量污染全局
var Person = (function(){
var name = '南玖'
function Person() {
console.log('work for qtt')
}
Person.prototype.work = function() {}
return Person
})()
- 循环体中创建闭包,保存变量
for(var i=0;i<5;i++){
(function(j){
setTimeOut(() => {
console.log(j)
},1000)
})(i)
}
推荐阅读:JavaScript深入之作用域与闭包
16.Object.is()与比较操作符==
、===
的区别?
==
会先进行类型转换再比较===
比较时不会进行类型转换,类型不同则直接返回falseObject.is()
在===
基础上特别处理了NaN
,-0
,+0
,保证-0与+0不相等,但NaN与NaN相等
==
操作符的强制类型转换规则
- 字符串和数字之间的相等比较,将字符串转换为数字之后再进行比较。
- 其他类型和布尔类型之间的相等比较,先将布尔值转换为数字后,再应用其他规则进行比较。
- null 和 undefined 之间的相等比较,结果为真。其他值和它们进行比较都返回假值。
- 对象和非对象之间的相等比较,对象先调用 ToPrimitive 抽象操作后,再进行比较。
- 如果一个操作值为 NaN ,则相等比较返回 false( NaN 本身也不等于 NaN )。
- 如果两个操作值都是对象,则比较它们是不是指向同一个对象。如果两个操作数都指向同一个对象,则相等操作符返回true,否则,返回 false。
'1' == 1 // true
'1' === 1 // false
NaN == NaN //false
+0 == -0 //true
+0 === -0 // true
Object.is(+0,-0) //false
Object.is(NaN,NaN) //true
17.call与apply、bind的区别?
实际上call与apply的功能是相同的,只是两者的传参方式不一样,而bind传参方式与call相同,但它不会立即执行,而是返回这个改变了this指向的函数。
18.说说你了解哪些前端本地存储?
推荐阅读:这一次带你彻底了解前端本地存储
19.说说JavaScript数组常用方法
向数组添加元素的方法:
- push:向数组的末尾追加 返回值是添加数据后数组的新长度,改变原有数组
- unshift:向数组的开头添加 返回值是添加数据后数组的新长度,改变原有数组
- splice:向数组的指定index处插入 返回的是被删除掉的元素的集合,会改变原有数组
向数组删除元素的方法:
- pop():从尾部删除一个元素 返回被删除掉的元素,改变原有数组
- shift():从头部删除一个元素 返回被删除掉的元素,改变原有数组
- splice:在index处删除howmany个元素 返回的是被删除掉的元素的集合,会改变原有数组
数组排序的方法:
- reverse():反转,倒置 改变原有数组
- sort():按指定规则排序 改变原有数组
数组迭代方法
参数: 每一项上运行的函数, 运行该函数的作用域对象(可选)
every()
对数组中的每一运行给定的函数,如果该函数对每一项都返回true,则该函数返回true
var arr = [10,30,25,64,18,3,9]
var result = arr.every((item,index,arr)=>{
return item>3
})
console.log(result) //false
some()
对数组中的每一运行给定的函数,如果该函数有一项返回true,就返回true,所有项返回false才返回false
var arr2 = [10,20,32,45,36,94,75]
var result2 = arr2.some((item,index,arr)=>{
return item<10
})
console.log(result2) //false
filter()
对数组中的每一运行给定的函数,会返回满足该函数的项组成的数组
// filter 返回满足要求的数组项组成的新数组
var arr3 = [3,6,7,12,20,64,35]
var result3 = arr3.filter((item,index,arr)=>{
return item > 3
})
console.log(result3) //[6,7,12,20,64,35]
map()
对数组中的每一元素运行给定的函数,返回每次函数调用的结果组成的数组
// map 返回每次函数调用的结果组成的数组
var arr4 = [1,2,3,4,5,6]
var result4 = arr4.map((item,index,arr)=>{
return `<span>${item}</span>`
})
console.log(result4)
/*[ '<span>1</span>',
'<span>2</span>',
'<span>3</span>',
'<span>4</span>',
'<span>5</span>',
'<span>6</span>' ]*/
forEach()
对数组中的每一元素运行给定的函数,没有返回值,常用来遍历元素
// forEach
var arr5 = [10,20,30]
var result5 = arr5.forEach((item,index,arr)=>{
console.log(item)
})
console.log(result5)
/*
10
20
30
undefined 该方法没有返回值
*/
reduce()
reduce()
方法对数组中的每个元素执行一个由你提供的reducer函数(升序执行),将其结果汇总为单个返回值
const array = [1,2,3,4]
const reducer = (accumulator, currentValue) => accumulator + currentValue;
// 1 + 2 + 3 + 4
console.log(array1.reduce(reducer));
20.JavaScript为什么要进行变量提升,它导致了什么问题?
变量提升的表现是,在变量或函数声明之前访问变量或调用函数而不会报错。
原因
JavaScript引擎在代码执行前有一个解析的过程(预编译),创建执行上线文,初始化一些代码执行时需要用到的对象。
当访问一个变量时,会到当前执行上下文中的作用域链中去查找,而作用域链的首端指向的是当前执行上下文的变量对象,这个变量对象是执行上下文的一个属性,它包含了函数的形参、所有的函数和变量声明,这个对象的是在代码解析的时候创建的。
首先要知道,JS在拿到一个变量或者一个函数的时候,会有两步操作,即解析和执行。
-
在解析阶段
JS会检查语法,并对函数进行预编译。解析的时候会先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来,变量先赋值为undefined,函数先声明好可使用。在一个函数执行之前,也会创建一个函数执行上下文环境,跟全局执行上下文类似,不过函数执行上下文会多出this、arguments和函数的参数。
- 全局上下文:变量定义,函数声明
- 函数上下文:变量定义,函数声明,this,arguments
-
在执行阶段,就是按照代码的顺序依次执行。
那为什么会进行变量提升呢?主要有以下两个原因:
- 提高性能
- 容错性更好
(1)提高性能 在JS代码执行之前,会进行语法检查和预编译,并且这一操作只进行一次。这么做就是为了提高性能,如果没有这一步,那么每次执行代码前都必须重新解析一遍该变量(函数),而这是没有必要的,因为变量(函数)的代码并不会改变,解析一遍就够了。
在解析的过程中,还会为函数生成预编译代码。在预编译时,会统计声明了哪些变量、创建了哪些函数,并对函数的代码进行压缩,去除注释、不必要的空白等。这样做的好处就是每次执行函数时都可以直接为该函数分配栈空间(不需要再解析一遍去获取代码中声明了哪些变量,创建了哪些函数),并且因为代码压缩的原因,代码执行也更快了。
(2)容错性更好 变量提升可以在一定程度上提高JS的容错性,看下面的代码:
a = 1
var a
console.log(a) //1
如果没有变量提升,这段代码就会报错
导致的问题
var tmp = new Date();
function fn(){
console.log(tmp);
if(false){
var tmp = 'hello nanjiu';
}
}
fn(); // undefined
在这个函数中,原本是要打印出外层的tmp变量,但是因为变量提升的问题,内层定义的tmp被提到函数内部的最顶部,相当于覆盖了外层的tmp,所以打印结果为undefined。
var tmp = 'hello nanjiu';
for (var i = 0; i < tmp.length; i++) {
console.log(tmp[i]);
}
console.log(i); // 13
由于遍历时定义的i会变量提升成为一个全局变量,在函数结束之后不会被销毁,所以打印出来13。
总结
- 解析和预编译过程中的声明提升可以提高性能,让函数可以在执行时预先为变量分配栈空间
- 声明提升还可以提高JS代码的容错性,使一些不规范的代码也可以正常执行
- 函数是一等公民,当函数声明与变量声明冲突时,变量提升时函数优先级更高,会忽略同名的变量声明
觉得文章不错,可以点个赞呀_ 欢迎关注南玖~