该篇文章整理了一些前端经典面试题,附带详解,涉及到JavaScript多方面知识点,满满都是干货~建议收藏阅读
如果这篇文章有帮助到你,️关注+点赞️鼓励一下作者,文章公众号首发,关注 前端南玖 第一时间获取最新的文章~
其中有7种基本数据类型:
ES5的5种:Null
,undefined
,Boolean
,Number
,String
,
ES6新增:Symbol
表示独一无二的值
ES10新增:BigInt
表示任意大的整数
一种引用数据类型:
Object
(本质上是由一组无序的键值对组成)
包含function
,Array
,Date
等。JavaScript不支持创建任何自定义类型的数据,也就是说JavaScript中所有值的类型都是上面8中之一。
相同点:
不同点:
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 却是一直流传下来。
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 可以准确的判断引用数据类型,它的原理是检测构造函数的
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
当一个函数被定义时,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 赋值,以保证对象实例的类型不被篡改。
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 的引用
在JavaScript中类型转换有三种情况:
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
对象
返回值
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() //''
简单来说,作用域是指程序中定义变量的区域,它决定了当前执行代码对变量的访问权限
当可执行代码内部访问变量时,会先查找当前作用域下有无该变量,有则立即返回,没有的话则会去父级作用域中查找…一直找到全局作用域。我们把这种作用域的嵌套机制称为
作用域链
详细知识可以看我之前的文章:JavaScript深入之作用域与闭包
this的绑定规则有四种:默认绑定,隐式绑定,显式绑定,new绑定.
详细知识可以看我之前的文章:this指向与call,apply,bind
在可迭代对象(具有 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循环:遍历对象自身的和继承的可枚举的属性, 不能直接获取属性值。可以中断循环。
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: 只能遍历数组,不能中断,没有返回值(或认为返回值是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: 只能遍历数组,不能中断,返回值是修改后的数组。
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]
每个函数(类)天生自带一个属性prototype
,属性值是一个对象,里面存储了当前类供实例
使用的属性和方法 「(显示原型)」
在浏览器默认给原型开辟的堆内存中有一个constructor
属性:存储的是当前类本身(️注意:自己开辟的堆内存中默认没有constructor
属性,需要自己手动添加)「(构造函数)」
每个对象都有一个__proto__
属性,这个属性指向当前实例所属类的原型
(不确定所属类,都指向Object.prototype
)「(隐式原型)」
当你试图获取一个对象的某个属性时,如果这个对象本身没有这个属性,那么它会去它的隐式原型__proto__
(也就是它的构造函数的显示原型prototype
)中查找。「(原型链)」
详细知识可以看我之前的文章:你一定要懂的JavaScript之原型与原型链
DOM0级模型: ,这种模型不会传播,所以没有事件流的概念,但是现在有的浏览器支持以冒泡的方式实现,它可以在网页中直接定义监听函数,也可以通过 js属性来指定监听函数。这种方式是所有浏览器都兼容的。
IE 事件模型: 在该事件模型中,一次事件共有两个过程,事件处理阶段,和事件冒泡阶段。事件处理阶段会首先执行目标元素绑定的监听事件。然后是事件冒泡阶段,冒泡指的是事件从目标元素冒泡到 document,依次检查经过的节点是否绑定了事件监听函数,如果有则执行。这种模型通过 attachEvent 来添加监听函数,可以添加多个监听函数,会按顺序依次执行。
DOM2 级事件模型: 在该事件模型中,一次事件共有三个过程,第一个过程是事件捕获阶段。捕获指的是事件从 document 一直向下传播到目标元素,依次检查经过的节点是否绑定了事件监听函数,如果有则执行。后面两个阶段和 IE 事件模型的两个阶段相同。这种事件模型,事件绑定的函数是 addEventListener,其中第三个参数可以指定事件是否在捕获阶段执行。
事件委托指的是把一个元素的事件委托到另外一个元素上。一般来讲,会把一个或者一组元素的事件委托到它的父层或者更外层元素上,真正绑定事件的是外层元素,当事件响应到需要绑定的元素上时,会通过事件冒泡机制从而触发它的外层元素的绑定事件上,然后在外层元素上去执行函数。
当事件发生在 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也可以阻止冒泡。
JavaScript会阻塞DOM的解析,因此也就会阻塞DOM的加载。所以有时候我们希望延迟JS的加载来提高页面的加载速度。
模块化的开发方式可以提高代码复用率,方便进行代码的管理。通常一个文件就是一个模块,有自己的作用域,只向外暴露特定的变量和函数。
第一种是 CommonJS
方案,它通过 require
来引入模块,通过 module.exports
定义模块的输出接口。
第二种是 AMD
方案,这种方案采用异步加载的方式来加载模块,模块的加载不影响后面语句的执行,所有依赖这个模块的语句都定义在一个回调函数里,等到加载完成后再执行回调函数。require.js 实现了 AMD 规范。
第三种是 CMD
方案,这种方案和 AMD
方案都是为了解决异步模块加载的问题,sea.js
实现了 CMD 规范。它和require.js
的区别在于模块定义时对依赖的处理不同和对依赖模块的执行时机的处理不同。
第四种方案是 ES6
提出的方案,使用 import
和 export
的形式来导入导出模块。
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.config()
指定引用路径等,用define()
定义模块,用require()
加载模块。
CMD是另一种js模块化方案,它与AMD很类似,不同点在于:AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。此规范其实是在sea.js推广过程中产生的。
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 模块输出的是值的引用。
import
,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import
有点像 Unix 系统的“符号连接”,原始值变了,import
加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
export
命令显式指定输出的代码,import
时采用静态命令的形式。即在import
时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”。CommonJS 加载的是一个对象(即module.exports
属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
推荐阅读前端模块化理解
推荐阅读探索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
一般比较两个对象会采用递归来比较
一个函数和对其周围(词法环境)的引用捆绑在一起(或者说函数被引用包围),这样一个组合就是闭包(「closure」)
函数执行分成两个阶段(预编译阶段和执行阶段)。
利用了函数作用域链的特性,一个函数内部定义的函数会将包含外部函数的活动对象添加到它的作用域链中,函数执行完毕,其执行作用域链销毁,但因内部函数的作用域链仍然在引用这个活动对象,所以其活动对象不会被销毁,直到内部函数被烧毁后才被销毁。
模块封装,防止变量污染全局
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深入之作用域与闭包
==
、===
的区别?==
会先进行类型转换再比较===
比较时不会进行类型转换,类型不同则直接返回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
实际上call与apply的功能是相同的,只是两者的传参方式不一样,而bind传参方式与call相同,但它不会立即执行,而是返回这个改变了this指向的函数。
推荐阅读:这一次带你彻底了解前端本地存储
参数: 每一项上运行的函数, 运行该函数的作用域对象(可选)
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));
变量提升的表现是,在变量或函数声明之前访问变量或调用函数而不会报错。
JavaScript引擎在代码执行前有一个解析的过程(预编译),创建执行上线文,初始化一些代码执行时需要用到的对象。
当访问一个变量时,会到当前执行上下文中的作用域链中去查找,而作用域链的首端指向的是当前执行上下文的变量对象,这个变量对象是执行上下文的一个属性,它包含了函数的形参、所有的函数和变量声明,这个对象的是在代码解析的时候创建的。
首先要知道,JS在拿到一个变量或者一个函数的时候,会有两步操作,即解析和执行。
在解析阶段
JS会检查语法,并对函数进行预编译。解析的时候会先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来,变量先赋值为undefined,函数先声明好可使用。在一个函数执行之前,也会创建一个函数执行上下文环境,跟全局执行上下文类似,不过函数执行上下文会多出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。
觉得文章不错,可以点个赞呀_ 欢迎关注南玖~
手机扫一扫
移动阅读更方便
你可能感兴趣的文章