大家好,我是编程小6,很高兴遇见你,有问题可以及时留言哦。
持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第4天,点击查看活动详情
又到了金九银十季,最近我也是奔波于各种面试。我自己总结整理了很多方向的前端面试题。借着国庆这个假期,也把这些题目总结分享给大家,也祝正在面试的朋友们能够拿到满意的offer。
(1)2022年我的面试万字总结(浏览器网络篇)
(2)2022年我的面试万字总结(CSS篇)
(3)2022年我的面试万字总结(HTML篇)
(5)2022年我的面试万字总结(JS篇下)
(6)2022年我的面试万字总结(代码篇)
(7)2022年我的面试万字总结(Vue上)
(8)2022年我的面试万字总结(Vue下)
(9)2022年我的面试万字总结(Vue3+TS)
(10)2022年我的面试万字总结(Node、webpack、性能优化)
(11)2022年我的面试万字总结(小程序、git)
JavaScript共有八种数据类型
基本数据类型: Undefined、Null、Boolean、Number、String、Symbol、BigInt。
引用数据类型:Object
其中 Symbol 和 BigInt 是ES6 中新增的数据类型:
在操作系统中,内存被分为栈区和堆区
在数据结构中:
数据的储存方式
然后判断数据类型的方法一般可以通过:typeof、instanceof、constructor、toString四种常用方法
不同类型的优缺点 | typeof | instanceof | constructor | Object.prototype.toString.call |
---|---|---|---|---|
优点 | 使用简单 | 能检测出引用类型 |
基本能检测所有的类型(除了null和undefined) | 检测出所有的类型 |
缺点 | 只能检测出除null外的基本数据类型和引用数据类型中的function | 不能检测出基本类型,且不能跨iframe | constructor易被修改,也不能跨iframe | IE6下,undefined和null均为Object |
typeof null 的结果是Object。
在 JavaScript 第一个版本中,所有值都存储在 32 位的单元中,每个单元包含一个小的 类型标签(1-3 bits) 以及当前要存储值的真实数据。类型标签存储在每个单元的低位中,共有五种数据类型:
000: object - 当前存储的数据指向一个对象。
1: int - 当前存储的数据是一个 31 位的有符号整数。
010: double - 当前存储的数据指向一个双精度的浮点数。
100: string - 当前存储的数据指向一个字符串。
110: boolean - 当前存储的数据是布尔值。
如果最低位是 1,则类型标签标志位的长度只有一位;如果最低位是 0,则类型标签标志位的长度占三位,为存储其他四种数据类型提供了额外两个 bit 的长度。
有两种特殊数据类型:
那也就是说null的类型标签也是000,和Object的类型标签一样,所以会被判定为Object。
计算机是通过二进制的方式存储数据的,所以计算机计算0.1+0.2的时候,实际上是计算的两个数的二进制的和。
在 Js中只有一种数字类型:Number,它的实现遵循IEEE 754标准,使用64位固定长度来表示,也就是标准的double双精度浮点数。在二进制科学表示法中,双精度浮点数的小数部分最多只能保留52位,再加上前面的1,其实就是保留53位有效数字,剩余的需要舍去,遵从“0舍1入”的原则。
根据这个原则,0.1和0.2的二进制数相加,再转化为十进制数就是:0.30000000000000004
。所以不相等
解决方法就是设置一个误差范围,通常称为“机器精度”。对JavaScript来说,这个值通常为2-52,在ES6中,提供了Number.EPSILON
属性,而它的值就是2-52,只要判断0.1+0.2-0.3
是否小于Number.EPSILON
,如果小于,就可以判断为0.1+0.2 ===0.3
function numberepsilon(arg1,arg2){
return Math.abs(arg1 - arg2) < Number.EPSILON;
}
console.log(numberepsilon(0.1 + 0.2, 0.3)); // true
因为 undefined 是一个标识符,所以可以被当作变量来使用和赋值,但是这样会影响 undefined 的正常判断。表达式 void ___ 没有返回值,因此返回结果是 undefined。void 并不改变表达式的结果,只是让表达式不返回值。因此可以用 void 0 来获得 undefined。
NaN 指“不是一个数字”(not a number),NaN 是一个“警戒值”(sentinel value,有特殊用途的常规值),用于指出数字类型中的错误情况,即“执行数学运算没有成功,这是失败后返回的结果”。
typeof NaN; // "number"
NaN 是一个特殊值,它和自身不相等,是唯一一个非自反的值。所谓的非自反就是说,NaN 与谁都不相等,包括它本身,但在 NaN != NaN 下会返回true
对于 ==
来说,如果对比双方的类型不一样,就会进行类型转换。假如对比 x
和 y
是否相同,就会进行如下判断流程:
null
和 undefined
,是的话就会返回 true
string
和 number
,是的话就会将字符串转换为 number
1 == '1'
↓
1 == 1
boolean
,是的话就会把 boolean
转为 number
再进行判断'1' == true
↓
'1' == 1
↓
1 == 1
object
且另一方为 string
、number
或者 symbol
,是的话就会把 object
转为原始类型再进行判断'1' == { name: 'js' } ↓'1' == '[object Object]'
其流程图如下:
为了将值转换为相应的基本类型值, 隐式转换会首先检查该值是否有valueOf()方法。如果有并且返回基本类型值,就使用该值进行强制类型转换。如果没有就使用 toString() 的返回值(如果存在)来进行强制类型转换。
如果 valueOf() 和 toString() 均不返回基本类型值,会产生 TypeError 错误。
以下这些是假值: undefined 、 null 、 false 、 +0、-0 和 NaN 、 ""
假值的布尔强制类型转换结果为 false。从逻辑上说,假值列表以外的都应该是真值。
|| 和 && 首先会对第一个操作数执行条件判断,如果其不是布尔值就先强制转换为布尔类型,然后再执行条件判断。
|| 和 && 返回它们其中一个操作数的值,而非条件判断的结果
在 JavaScript 中,基本类型是没有属性和方法的,但是为了便于操作基本类型的值,在调用基本类型的属性或方法时 JavaScript 会在后台隐式地将基本类型的值转换为对象。如:
const a = "abc";
a.length; // 3
在访问'abc'.length
时,JavaScript 将'abc'
在后台转换成String('abc')
,然后再访问其length
属性。
在 if 语句、逻辑语句、数学运算逻辑、== 等情况下都可能出现隐式类型转换。
坑: 判断时, 尽量不要用 = =
, 要用 = = =
( 两个等号判断, 如果类型不同, 默认会进行隐式类型转换再比较)
this是一个在运行时才进行绑定的引用
,在不同的情况下它可能会被绑定不同的对象。
new绑定优先级 > 显示绑定优先级 > 隐式绑定优先级 > 默认绑定优先级
Map | Object | |
---|---|---|
意外的键 | Map默认情况不包含任何键,只包含显式插入的键。 | Object 有一个原型, 原型链上的键名有可能和自己在对象上的设置的键名产生冲突。 |
键的类型 | Map的键可以是任意值,包括函数、对象或任意基本类型。 | Object 的键必须是 String 或是Symbol。 |
键的顺序 | Map 中的 key 是有序的。因此,当迭代的时候, Map 对象以插入的顺序返回键值。 | Object 的键是无序的 |
Size | Map 的键值对个数可以轻易地通过size 属性获取 | Object 的键值对个数只能手动计算 |
迭代 | Map 是 iterable 的,所以可以直接被迭代。 | 迭代Object需要以某种方式获取它的键然后才能迭代。 |
性能 | 在频繁增删键值对的场景下表现更好。 | 在频繁添加和删除键值对的场景下未作出优化。 |
JSON 是一种基于文本的轻量级的数据交换格式。它可以被任何的编程语言读取和作为数据格式来传递。
在项目开发中,使用 JSON 作为前后端数据交换的方式。在前端通过将一个符合 JSON 格式的数据结构序列化为 JSON 字符串,然后将它传递到后端,后端通过 JSON 格式的字符串解析后生成对应的数据结构,以此来实现前后端数据的一个传递。
因为 JSON 的语法是基于 js 的,因此很容易将 JSON 和 js 中的对象弄混,但是应该注意的是 JSON 和 js 中的对象不是一回事,JSON 中对象格式更加严格,比如说在 JSON 中属性值不能为函数,不能出现 NaN 这样的属性值等,因此大多数的 js 对象是不符合 JSON 对象的格式的。
在 js 中提供了两个函数来实现 js 数据结构和 JSON 格式的转换处理,
console.log(String("abc")); // abc
console.log(JSON.stringify("abc")); // "abc"
console.log(String({ key: "value" })); // [object Object]
console.log(JSON.stringify({ key: "value" })); // {"key":"value"}
console.log(String([1, 2, 3])); // 1,2,3
console.log(JSON.stringify([1, 2, 3])); // [1,2,3]
const obj = {
title: "devpoint",
toString() {
return "obj";
},
};
console.log(String(obj)); // obj
console.log(JSON.stringify(obj)); // {"title":"devpoint"}
JSON.stringify
。toString
方法被重写,则需要使用String()。String()
将变量转换为字符串。一个拥有 length 属性和若干索引属性的对象就可以被称为类数组对象,类数组对象和数组类似,但是不能调用数组的方法。
常见的类数组对象有 arguments 和 DOM 方法的返回结果,还有一个函数也可以被看作是类数组对象,因为它含有 length 属性值,代表可接收的参数个数。
常见的类数组转换为数组的方法有这样几种:
Array.prototype.slice.call(arrayLike);
Array.prototype.splice.call(arrayLike, 0);
Array.prototype.concat.apply([], arrayLike);
Array.from(arrayLike);
Unicode
是编码字符集(字符集),而UTF-8
、UTF-16
、UTF-32
是字符集编码(编码规则);UTF-16
使用变长码元序列的编码方式,相较于定长码元序列的UTF-32
算法更复杂,甚至比同样是变长码元序列的UTF-8
也更为复杂,因为其引入了独特的代理对这样的代理机制;UTF-8
需要判断每个字节中的开头标志信息,所以如果某个字节在传送过程中出错了,就会导致后面的字节也会解析出错;而UTF-16
不会判断开头标志,即使错也只会错一个字符,所以容错能力教强;UTF-8
就比UTF-16
节省了很多空间;而如果字符内容全部是中文这样类似的字符或者混合字符中中文占绝大多数,那么UTF-16
就占优势了,可以节省很多空间;现代计算机中数据都是以二进制的形式存储的,即0、1两种状态,计算机对二进制数据进行的运算加减乘除等都是叫位运算,即将符号位共同参与运算的运算。
常见的位运算有以下几种:
运算符 | 描述 | 运算规则 | |
---|---|---|---|
& |
与 | 两个位都为1时,结果才为1 | |
` | 或 | 两个位都为0时,结果才为0 | |
^ |
异或 | 两个位相同为0,相异为1 | |
~ |
取反 | 0变1,1变0 | |
<< |
左移 | 各二进制位全部左移若干位,高位丢弃,低位补0 | |
>> |
右移 | 各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃 |
arguments
是一个对象,它的属性是从 0 开始依次递增的数字,还有callee
和length
等属性,与数组相似;但是它却没有数组常见的方法属性,如forEach
, reduce
等,所以叫它们类数组。
要遍历类数组,有三个方法:
(1)将数组的方法应用到类数组上,这时候就可以使用call
和apply
方法,如:
function foo(){
Array.prototype.forEach.call(arguments, a => console.log(a))
}
(2)使用Array.from方法将类数组转化成数组:
function foo(){
const arrArgs = Array.from(arguments)
arrArgs.forEach(a => console.log(a))
}
(3)使用展开运算符将类数组转化成数组
function foo(){
const arrArgs = [...arguments]
arrArgs.forEach(a => console.log(a))
}
尾调用指的是函数的最后一步调用另一个函数。代码执行是基于执行栈的,所以当在一个函数里调用另一个函数时,会保留当前的执行上下文,然后再新建另外一个执行上下文加入栈中。使用尾调用的话,因为已经是函数的最后一步,所以这时可以不必再保留当前的执行上下文,从而节省了内存,这就是尾调用优化。
但是 ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。
use strict 是一种 ECMAscript5 添加的(严格模式)运行模式,这种模式使得 Javascript 在更严格的条件下运行。设立严格模式的目的如下:
区别:
两者对比:强类型语言在速度上可能略逊色于弱类型语言,但是强类型语言带来的严谨性可以有效地帮助避免许多错误。
(1)解释型语言 使用专门的解释器对源程序逐行解释成特定平台的机器码并立即执行。是代码在执行时才被解释器一行行动态翻译和执行,而不是在执行之前就完成翻译。解释型语言不需要事先编译,其直接将源代码解释成机器码并立即执行,所以只要某一平台提供了相应的解释器即可运行该程序。其特点总结如下
(2)编译型语言 使用专门的编译器,针对特定的平台,将高级语言源代码一次性的编译成可被该平台硬件执行的机器码,并包装成该平台所能识别的可执行性程序的格式。在编译型语言写的程序执行之前,需要一个专门的编译过程,把源代码编译成机器语言的文件,如exe格式的文件,以后要再运行时,直接使用编译结果即可,如直接运行exe文件。因为只需编译一次,以后运行时不需要编译,所以编译型语言执行效率高。其特点总结如下:
两者主要区别在于: 后者源程序编译后即可在该平台运行,前者是在运行期间才编译。所以后者运行速度快,前者跨平台性好。
for…of 是ES6新增的遍历方式,允许遍历一个含有iterator接口的数据结构(数组、对象等)并且返回各项的值,和ES3中的for…in的区别如下
总结: for...in 循环主要是为了遍历对象而生,不适用于遍历数组;for...of 循环可以用来遍历数组、类数组对象,字符串、Set、Map 以及 Generator 对象。
(1)AJAX Ajax 即“AsynchronousJavascriptAndXML”(异步 JavaScript 和 XML),是指一种创建交互式网页应用的网页开发技术。它是一种在无需重新加载整个网页的情况下,能够更新部分网页的技术。通过在后台与服务器进行少量数据交换,Ajax 可以使网页实现异步更新。这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新。传统的网页(不使用 Ajax)如果需要更新内容,必须重载整个网页页面。其缺点如下:
(2)Fetch fetch号称是AJAX的替代品,是在ES6出现的,使用了ES6中的promise对象。Fetch是基于promise设计的。Fetch的代码结构比起ajax简单多。fetch不是ajax的进一步封装,而是原生js,没有使用XMLHttpRequest对象。
fetch的优点:
fetch的缺点:
(3)Axios Axios 是一种基于Promise封装的HTTP客户端,其特点如下:
方法 | 是否改变原数组 | 特点 |
---|---|---|
forEach() | 否 | 数组方法,不改变原数组的长度,没有返回值 |
map() | 否 | 数组方法,不改变原数组的长度,有返回值,可链式调用 |
filter() | 否 | 数组方法,过滤数组,返回包含符合条件的元素的数组,可链式调用 |
for...of | 否 | for...of遍历具有Iterator迭代器的对象的属性,返回的是数组的元素、对象的属性值,不能遍历普通的obj对象,将异步循环变成同步循环 |
every() 和 some() | 否 | 数组方法,some()只要有一个是true,便返回true;而every()只要有一个是false,便返回false. |
find() 和 findIndex() | 否 | 数组方法,find()返回的是第一个符合条件的值;findIndex()返回的是第一个返回条件的值的索引值 |
reduce() 和 reduceRight() | 否 | 数组方法,reduce()对数组正序操作;reduceRight()对数组逆序操作 |
这方法都是用来遍历数组的,两者区别如下:
浅拷贝
常见的浅拷贝:
深拷贝
深拷贝开辟一个新的栈,两个对象属完成相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性
常见的深拷贝方式有:
Lodash是一个一致性、模块化、高性能的 JavaScript 实用工具库。
array
中的前 n
个元素去掉,然后返回剩余的部分.LHS (Left-hand Side)
和 RHS (Right-hand Side)
,是在代码执行阶段 JS 引擎操作变量的两种方式,二者区别就是对变量的查询目的是 变量赋值 还是 查询 。
LHS 可以理解为变量在赋值操作符(=)
的左侧,例如 a = 1
,当前引擎对变量 a
查找的目的是变量赋值。这种情况下,引擎不关心变量 a
原始值是什么,只管将值 1
赋给 a
变量。
RHS 可以理解为变量在赋值操作符(=)
的右侧,例如:console.log(a)
,其中引擎对变量a
的查找目的就是 查询,它需要找到变量 a
对应的实际值是什么,然后才能将它打印出来。
includes可以检测NaN
,indexOf不能检测NaN
,includes内部使用了Number.isNaN
对NaN
进行了匹配
模块化 | 代表应用 | 特点 |
---|---|---|
AMD | require.js | 1、AMD的api默认一个当多个用 2、依赖前置,异步执行 |
CMD | sea.js | 1、CMD的api严格区分,推崇职责单一 2、依赖就近,按需加载,同步执行 |
方案一:重写toString()或valueOf()
let a = {
i: 1,
toString: function () {
return a.i++;
}
}
console.log(a == 1 && a == 2 && a == 3); // true
方案二:数组
数组的toString接口默认调用数组的join方法,重写join方法。定义a为数字,每次比较时就会调用 toString()方法,我们把数组的shift方法覆盖toString即可:
let a = [1,2,3];
a.toString = a.shift;
console.log(a == 1 && a == 2 && a == 3); // true
当然把toString改为valueOf也是一样效果:
let a = [1,2,3];
a. valueOf = a.shift;
console.log(a == 1 && a == 2 && a == 3); // true
方案三:使用Object.defineProperty()
Object.defineProperty()用于定义对象中的属性,接收三个参数:object对象、对象中的属性,属性描述符。属性描述符中get:访问该属性时自动调用。
var _a = 1;
Object.defineProperty(this,'a',{
get:function(){
return _a++
}
})
console.log(a===1 && a===2 && a===3)//true
MUL表示数的简单乘法。在这种技术中,将一个值作为参数传递给一个函数,而该函数将返回另一个函数,将第二个值传递给该函数,然后重复继续。例如:xyz可以表示为
const mul = x => y => z => x * y * z
console.log(mul(1)(2)(3)) // 6
对于算法来说 无非就是时间换空间 空间换时间
保证一个类仅有一个实例,并提供一个访问它的全局访问点。实现的方法为先判断实例存在与否,如果存在则直接返回,如果不存在就创建了再返回,这就确保了一个类只有一个实例对象。
定义一系列的算法,把他们一个个封装起来,并且使他们可以相互替换。
为一个对象提供一个代用品或占位符,以便控制对它的访问。
通过一个中介者对象,其他所有的相关对象都通过该中介者对象来通信,而不是相互引用,当其中的一个对象发生改变时,只需要通知中介者对象即可。通过中介者模式可以解除对象与对象之间的紧耦合关系。
在不改变对象自身的基础上,在程序运行期间给对象动态地添加方法。
forEach是不能通过break
或者return
来实现跳出循环的,为什么呢?实现过forEach的同学应该都知道,forEach的的回调函数形成了一个作用域,在里面使用return
并不会跳出,只会被当做continue
可以利用try catch
function getItemById(arr, id) {
var item = null;
try {
arr.forEach(function (curItem, i) {
if (curItem.id == id) {
item = curItem;
throw Error();
}
})
} catch (e) {
}
return item;
}
1、使用 location.href:window.location.href ="url"
2、使用 location.replace: window.location.replace("url");
上拉加载
上拉加载的本质是页面触底,或者快要触底时的动作
判断页面触底我们需要先了解一下下面几个属性
scrollTop
:滚动视窗的高度距离window
顶部的距离,它会随着往上滚动而不断增加,初始值是0,它是一个变化的值clientHeight
:它是一个定值,表示屏幕可视区域的高度;scrollHeight
:页面不能滚动时也是存在的,此时scrollHeight等于clientHeight。scrollHeight表示body
所有元素的总长度(包括body元素自身的padding)综上我们得出一个触底公式:
scrollTop + clientHeight >= scrollHeight
下拉刷新
下拉刷新的本质是页面本身置于顶部时,用户下拉时需要触发的动作
关于下拉刷新的原生实现,主要分成三步:
touchstart
事件,记录其初始位置的值,e.touches[0].pageY
;touchmove
事件,记录并计算当前滑动的位置值与初始位置值的差值,大于0
表示向下拉动,并借助CSS3的translateY
属性使元素跟随手势向下滑动对应的差值,同时也应设置一个允许滑动的最大值;touchend
事件,若此时元素滑动达到最大值,则触发callback
,同时将translateY
重设为0
,元素回到初始位置JavaScript 中的数组存储大致需要分为两种情况:
温馨提示:可以想象一下连续的内存空间只需要根据索引(指针)直接计算存储位置即可。如果是哈希映射那么首先需要计算索引值,然后如果索引值有冲突的场景下还需要进行二次查找(需要知道哈希的存储方式)。
深度回答
浏览器在加载页面会把代码放在栈内存( ECStack )中执行,函数进栈执行会产生一个私有上下文( EC ),此上下文能保护里面的使用变量( AO )不受外界干扰,并且如果当前执行上下文中的某些内容,被上下文以外的内容占用,当前上下文不会出栈释放,这样可以保存里面的变量和变量值,所以我认为闭包是一种保存和保护内部私有变量的机制。
闭包有两个常用的用途;
在实际的项目中,会基于闭包把自己编写的模块内容包裹起来,这样编写就可以保护自己的代码是私有的,防止和全局变量或者是其他的代码冲突,这一点是利用保护机制。
但是不建议过多的使用闭包,因为使用不被释放的上下文,是占用栈内存空间的,过多的使用会导致导致内存泄漏。
解决闭包带来的内存泄漏问题的方法是:使用完闭包函数后手动释放。
return
回一个函数形成私有上下文
进栈执行
一系列操作
(1). 初始化作用域链(两头<当前作用域,上级作用域>)
(2). 初始化this
(3). 初始化arguments
(4). 赋值形参
(5). 变量提升
(6). 代码执行
正常情况下,代码执行完成之后,私有上下文出栈被回收。但是遇到特殊情况,如果当前私有上下文执行完成之后中的某个东西被执行上下文以外的东西占用,则当前私有上下文就不会出栈释放,也就是形成了不被销毁的上下文,闭包。
(1)全局执行上下文
任何不在函数内部的都是全局执行上下文,它首先会创建一个全局的window对象,并且设置this的值等于这个全局对象,一个程序中只有一个全局执行上下文。
(2)函数执行上下文
当一个函数被调用时,就会为该函数创建一个新的执行上下文,函数的上下文可以有任意多个。
(3) eval
函数执行上下文
执行在eval函数中的代码会有属于他自己的执行上下文,不过eval函数不常使用,不做介绍。
创建阶段 → 执行阶段 → 回收阶段
创建阶段
(1)this绑定
(2)创建词法环境组件
(3)创建变量环境组件
执行阶段
在这阶段,执行变量赋值、代码执行
如果 Javascript
引擎在源代码中声明的实际位置找不到变量的值,那么将为其分配 undefined
值
回收阶段
执行上下文出栈等待虚拟机回收执行上下文
直接写在script标签的JS代码,都在全局作用域。在全局作用域下声明的变量叫做全局变量(在块级外部定义的变量)。
全局变量在全局的任何位置下都可以使用;全局作用域中无法访问到局部作用域的中的变量。
全局作用域在页面打开的时候创建,在页面关闭时销毁。
所有 window 对象的属性拥有全局作用域
var和function命令声明的全局变量和函数是window对象的属性和方法
let命令、const命令、class命令声明的全局变量,不属于window对象的属性
当在js
中使用一个变量的时候,首先js
引擎会尝试在当前作用域下去寻找该变量,如果没找到,再到它的上层作用域寻找,以此类推直到找到该变量或是已经到了全局作用域,这样的变量作用域访问的链式结构, 被称之为作用域链
深度回答
作用域链的本质上是一个指向变量对象的指针列表。变量对象是一个包含了执行环境中所有变量和函数的对象。作用域链的前端始终都是当前执行上下文的变量对象。全局执行上下文的变量对象(也就是全局对象)始终是作用域链的最后一个对象。
作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,可以访问到外层环境的变量和函数。
作用域的一个常见运用场景之一,就是 模块化。
由于 javascript 并未原生支持模块化导致了很多令人口吐芬芳的问题,比如全局作用域污染和变量名冲突,代码结构臃肿且复用性不高。在正式的模块化方案出台之前,开发者为了解决这类问题,想到了使用函数作用域来创建模块的方案。
JS 引擎在运行一份代码的时候,会按照下面的步骤进行工作:
1.把变量的声明提升到当前作用域的最前面,只会提升声明,不会提升赋值
2.把函数的声明提升到当前作用域的最前面,只会提升声明,不会提升调用
3.先提升 function,在提升 var
变量提升
简单说就是在 JavaScript 代码执行前引擎会先进行预编译,预编译期间会将变量声明与函数声明
提升至其对应作用域的最顶端
,函数内声明的变量
只会提升至该函数作用域最顶层
,当函数内部定义的一个变量与外部相同时
,那么函数体内的这个变量就会被上升到最顶端
。
函数提升
函数提升只会提升函数声明式写法,函数表达式的写法不存在函数提升
函数提升的优先级大于变量提升的优先级,即函数提升在变量提升之上
作用域链是可以延长的。
延长作用域链: 执行环境的类型只有两种,全局和局部(函数)。但是有些语句可以在作用域链的前端临时增加一个变量对象,该变量对象会在代码执行后被移除。
具体来说就是执行这两个语句时,作用域链都会得到加强
JS 环境中分配的内存, 一般有如下生命周期:
内存分配:当我们声明变量、函数、对象的时候,系统会自动为他们分配内存
内存使用:即读写内存,也就是使用变量、函数等
内存回收:使用完毕,由垃圾回收自动回收不再使用的内存
全局变量一般不会回收, 一般局部变量的的值, 不用了, 会被自动回收掉
垃圾回收:JavaScript代码运行时,需要分配内存空间来储存变量和值。当变量不在参与运行时,就需要系统收回被占用的内存空间,这就是垃圾回收。
回收机制:
1.引用计数法
obj1
和obj2
通过属性进行相互引用,两个对象的引用次数都是2。当使用循环计数时,由于函数执行完后,两个对象都离开作用域,函数执行结束,obj1
和obj2
还将会继续存在,因此它们的引用次数永远不会是0,就会引起循环引用。2.标记清除法
现代的浏览器已经不再使用引用计数算法了。
现代浏览器通用的大多是基于标记清除算法的某些改进算法,总体思想都是一致的。
虽然浏览器可以进行垃圾自动回收,但是当代码比较复杂时,垃圾回收所带来的代价比较大,所以应该尽量减少垃圾回收。
object
进行优化: 对象尽量复用,对于不再使用的对象,就将其设置为null,尽快被回收。是指由于疏忽或错误造成程序未能释放已经不再使用的内存
以下四种情况会造成内存的泄漏:
函数式编程是一种"编程范式"(programming paradigm),一种编写程序的方法论
主要的编程范式有三种:命令式编程,声明式编程和函数式编程
相比命令式编程,函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而非设计一个复杂的执行过程
优点
缺点
纯函数是对给定的输入返还相同输出的函数,并且要求你所有的数据都是不可变的,即纯函数=无状态+数据不可变
特性:
优势:
在函数式编程中,有一个很重要的概念就是函数组合,实际上就是把处理的函数数据像管道一样连接起来,然后让数据穿过管道连接起来,得到最终的结果。
组合函数,其实大致思想就是将 多个函数组合成一个函数,c(b(a(a(1)))) 这种写法简写为 compose(c, b, a, a)(x) 。但是注意这里如果一个函数都没有传入,那就是传入的是什么就返回什么,并且函数的执行顺序是和传入的顺序相反的。
var compose = (...funcs) => {
// funcs(数组):记录的是所有的函数
// 这里其实也是利用了柯里化的思想,函数执行,生成一个闭包,预先把一些信息存储,供下级上下文使用
return (x) => {
var len = funcs.length;
// 如果没有函数执行,直接返回结果
if (len === 0) return x;
if (len === 1) funcs[0](x);
return funcs.reduceRight((res, func) => {
return func(res);
}, x);
};
};
var resFn = compose(c, b, a, a);
resFn(1);
组合函数的思想,在很多框架中也被使用,例如:redux,实现效果来说是其实和上面的代码等价。
惰性载入表示函数执行的分支只会在函数第一次掉用的时候执行,在第一次调用过程中,该函数会被覆盖为另一个按照合适方式执行的函数,这样任何对原函数的调用就不用再经过执行的分支了
惰性函数相当于有记忆的功能一样,当它已经判断了一遍的话,第二遍就不会再判断了。
比如现在要求写一个test函数,这个函数返回首次调用时的new Date().getTime(),注意是首次,而且不允许有全局变量的污染
//一般会这样实现
var test = (function () {
var t = null;
return function () {
if (t) {
return t;
}
t = new Date().getTime();
return t;
}
})();
// 用惰性函数实现
var test = function () {
var t = new Date().getTime();
test = function () {
return t;
}
return test();
}
console.log(test());
console.log(test());
console.log(test());
高阶函数是指使用其他函数作为参数、或者返回一个函数作为结果的函数。
柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。
函数柯里化的好处:
(1)参数复用:需要输入多个参数,最终只需输入一个,其余通过 arguments 来获取
(2)提前确认:避免重复去判断某一条件是否符合,不符合则 return 不再继续执行下面的操作
(3)延迟运行:避免重复的去执行程序,等真正需要结果的时候再执行
使用 "箭头" ( => ) 来定义函数. 箭头函数相当于匿名函数, 并且简化了函数定义
箭头函数的特征:
如果一个函数在内部调用自身本身,这个函数就是递归函数
其核心思想是把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解
一般来说,递归需要有边界条件、递归前进阶段和递归返回阶段。当边界条件不满足时,递归前进;当边界条件满足时,递归返回
优点:结构清晰、可读性强
缺点:效率低、调用栈可能会溢出,其实每一次函数调用会在内存栈中分配空间,而每个进程的栈的容量是有限的,当调用的层次太多时,就会超出栈的容量,从而导致栈溢出。
尾递归,即在函数尾位置调用自身(或是一个尾调用本身的其他函数等等)。
在递归调用的过程当中系统为每一层的返回点、局部量等开辟了栈来存储,递归次数过多容易造成栈溢出
这时候,我们就可以使用尾递归,即一个函数中所有递归形式的调用都出现在函数的末尾,对于尾递归来说,由于只存在一个调用记录,所以永远不会发生"栈溢出"错误
传递复杂数据类型传递的是引用的地址,修改会改变
简单数据类型传递的是具体的值,不会相互影响
/* let a = 8
function fn(a) {
a = 9
}
fn(a)
console.log(a) // 8 */
let a = { age: 8 }
function fn(a) {
a.age = 9
}
fn(a)
console.log(a.age) // 9
函数声明: funtion开头,有函数提升
函数表达式: 不是funtion开头,没有函数提升
概念
函数缓存,就是将函数运算过的结果进行缓存
本质上就是用空间(缓存存储)换时间(计算过程)
常用于缓存数据计算结果和缓存对象
如何实现
实现函数缓存主要依靠闭包、柯里化、高阶函数
应用场景
共同点 :
this
要指向的对象,如果如果没有这个参数或参数为undefined
或null
,则默认指向全局window
不同点:
apply
和call
是一次性传入参数,而bind
可以分为多次传入bind
是返回绑定this之后的函数应用场景