前端面试知识点javascript
undefined 不是关键词可能被修改 (指局部环境下) (现代浏览器已经修改undefined的 non-writable non-configurable的值 全局环境没法被修改)html
null 是关键词前端
void 0 为 unfefinedjava
String 最大长度为 2^53 - 1 charAt、charCodeAt、length 等方法针对的都是 UTF16 编码 JavaScript 中的字符串是永远没法变动的node
Number 类型中有效的整数范围是 -0x1fffffffffffff 至 0x1fffffffffffff面试
一样根据浮点数的定义,非整数的 Number 类型没法用 == 或 === (因此 0.1+0.2 == 0.3 //false) Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON // true 检查等式左右两边差的绝对值是否小于最小精度算法
Number、String 和 Boolean,三个构造器是两用的,当跟 new 搭配时,它们产生对象,当直接调用时,它们表示强制类型转换。 使用 new Number(3) 获得的是对象shell
在 JavaScript 中,没有任何方法能够更改私有的 Class 属性,所以 Object.prototype.toString 是能够准确识别对象对应的基本类型的方法,它比 instanceof 更加准确。api
在 JavaScript 标准中,规定了 ToPrimitive 函数,它是对象类型到基本类型的转换(即,拆箱转换)。数组
ToPrimitive 为内部函数,在将对象转换成原始值则会调用toPrimitive(input,preferedType?)
其中 input 为输入的值 preferedType是指望转换的类型
基本流程以下
若是转换的类型是String 2和3 顺序会变化
在操做符中,==,排序运算符,加减乘除,在对非原始值进行操做时,都会调用内部的toPrimitive()方法
tips: 加上操做符会将preferedType当作Number
[] + [] = ""
[] + {} = [object object]
{} + [] = 0
复制代码
装箱:将原始类型转成对象(原始类型有七种 boolean, number, string, undefined,null,bigInt,symbol)
var a=10 ,b="javascript" , c=true;
var o_a=new Number(a);
复制代码
拆箱: 将引用类型对象转换为对应的值类型对象,它是经过引用类型的valueOf()或者toString()方法来实现的。若是是自定义的对象,你也能够自定义它的valueOf()/tostring()方法,实现对这个对象的拆箱。
var a=10;
var o_a=new Number(a);
var b=o_a.valueOf();
复制代码
getter: 函数或 undefined, 在读取属性值被调用。
setter: 函数或undefined,在设置属性值时被调用。
enumerable: 决定for in可否枚举该属性
configurable: 决定该属性可否被删除或改变特征值
var a = { b: 1}
a.c = 2
Object.getOwnPropertyDescriptor(a,"b")
// {value: 1, writable: true, enumerable: true, configurable: true} 默认都为true
Object.getOwnProertyDescriptor(a,"c")
// {value: 2, writable: true, enumerable: true, configurable: true}
复制代码
var o = { a: 1 }
Object.defineProperty(o, "b", {value: 2, writable: false, enumerable: false, configurable: true})
复制代码
object.create
根据指定的原型建立新对象,原型能够是nulll。object.getPrototypeOf
获取一个对象的原型。object.setPrototypeOf
设置一个对象的原型。在 ES3 和以前的版本,JS 中类的概念是至关弱的,它仅仅是运行时的一个字符串属性。
在 ES5 开始,[[class]] 私有属性被 Symbol.toStringTag 代替
使用 Symbol.toStringTag 来自定义 Object.prototype.toString 的行为:
var o = { [Symbol.toStringTag]: "myObject" }
console.log(o + "");
复制代码
宿主对象(Host Objects): 由 javaScript 宿主环境提供的对象,它们的行为彻底由宿主环境决定
内置对象(Built-in Object): 由 JavaScript 语言提供的对象。 (运行时runtime)
固有对象(Intrinsic Object): 由标准规定,随着JavaScript运行时建立而自动建立的对象实例
原生对象(Native Object): 能够由用户经过 Array、RegExp 等内置构造器或者特殊语法建立的对象。
普通对象(Ordinary Object): 由{}语法、Object 构造器或者class关键词定义类建立的对象,它可以被原型继承
Tips
function a() {}
a() // 包含[[call]]私有字段对象 表示能够做为函数被调用
new a() // 包含 [[construct]] 构造器对象,能够做为构造器被调用
// 对于部分宿主和内置对象两则有时效果不相同 好比 date image
// 实现 私有变量
function cls() {
this.a = 100;
return {
getValue: ()=> this.a
}
}
var b = new cls;
b.getValue() // 100
// a的值在外面没法访问到
//如何建立一个对象
//字面量建立
var a = {}
// dom api
var d = document.createElement('p')
// 内置对象
var e = Object.create(null)
// 装箱
var f = Object(null)
复制代码
setTimeout会单独生产一个新的宏观任务,在上一个宏观任务执行完毕后执行。
ES3
ES5
ES2018
this
this 是执行上下文中很重要的一个组成部分。同一个函数调用方式不一样,获得的 this 值也不一样
普通函数的 this 值由“调用它所使用的引用”决定,其中奥秘就在于:咱们获取函数的表达式,它实际上返回的并不是函数自己,而是一个 Reference 类型(记得咱们在类型一章讲过七种标准类型吗,正是其中之一)。Reference 类型由两部分组成:一个对象和一个属性值。不难理解 o.showThis 产生的 Reference 类型,即由对象 o 和属性“showThis”构成。
bind 、call、apply 函数经过函数调用时传入 this
javascript 标准 定义了 [[thisMode]]私有属性。[[thisMode]]有三个取值
lexical : 表示从上下文中找this, 这对应了箭头函数
Global: 表示当this为undefined时,取全局对象,对应了普通函数。
Strict: 当严格模式时使用,this严格按照调用时传入的值,可能为null或者undefined。
Class 默认按照strict模式执行
"use strict"
function showThis() {
console.log(this);
}
var o = {
showThis: showThis
}
showThis(); // undefined 严格模式下 this按照传入的值 全部未定义
o.showThis(); // o
复制代码
函数建立新的执行上下文中的词法环境记录时,会根据 [[thisMode]] 来标记新纪录 [[ThisBindingStatus]] 私有属性。
代码执行遇到 this 时,会逐层检查当前词法环境记录中的 [[ThisBindingStatus]],当找到有 this 的环境记录时获取 this 的值。
这样的规则的实际效果是,嵌套的箭头函数中的代码都指向外层 this。
箭头函
Completion Record 用于描述异常、跳出等语句执行过程
Completion Record 表示一个语句执行完以后的结果,它有三个字段:
JavaScript 正是依靠语句的 Completion Record 类型,方才能够在语句的复杂嵌套结构中,实现各类控制。
普通语句执行后,会获得 [[type]] 为 normal 的 Completion Record,JavaScript 引擎遇到这样的 Completion Record,会继续执行下一条语句。
这些语句中,只有表达式语句会产生 [[value]],固然,从引擎控制的角度,这个 value 并无什么用处。
var i = 1 // 该语句 [[type]]:noraml [[value]]: empty [[target]]: empty
// undefined
{//语句块
var i = 1; // [[type]]:noraml [[value]]: empty [[target]]: empty
return i; // [[type]]:return [[value]]: 1 [[target]]: empty
} // return , 1, empty
//控制型语句
复制代码
穿透: 被跳过 | 消费:被执行 | 特殊处理:特殊处理
[[target]]
实际上,任何 JavaScript 语句是能够加标签的,在语句前加冒号便可:
firstStatement: var i = 1;
复制代码
大部分时候,这个东西相似于注释,没有任何用处。惟一有做用的时候是:与完成记录类型中的 target 相配合,用于跳出多层循环。
outer: while(true) {
inner: while(true) {
break outer;
}
}
console.log("finished")
复制代码
在 JavaScript 中,咱们把不带控制能力的语句称为普通语句。
由于 JavaScript 语句存在着嵌套关系,因此执行过程实际上主要在一个树形结构上进行, 树形结构的每个节点执行后产生 Completion Record,根据语句的结构和 Completion Record,JavaScript 实现了各类分支和跳出逻辑。
词法规定了语言的最小语义单元:token
首先,JavaScript 有两种源文件,一种叫作脚本,一种叫作模块。这个区分是在 ES6 引入了模块机制开始的,在 ES5 和以前的版本中,就只有一种源文件类型(就只有脚本)。
脚本是能够由浏览器或者 node 环境引入执行的,而模块只能由 JavaScript 代码用 import 引入执行
从概念上,咱们能够认为脚本具备主动性的 JavaScript 代码段,是控制宿主完成必定任务的代码;而模块是被动性的 JavaScript 代码段,是等待被调用的库。
现代浏览器能够支持用 script 标签引入模块或者脚本,若是要引入模块,必须给 script 标签添加 type=“module”。若是引入脚本,则不须要 type。
<script type="module" src="xxxxx.js"></script>
复制代码
export 有一种特殊的用法,就是跟 default 联合使用。export default 表示导出一个默认变量值,它能够用于 function 和 class。这里导出的变量是没有名称的,可使用import x from "./a.js"这样的语法,在模块中引入。
函数体其实也是一个语句的列表。跟脚本和模块比起来,函数体中的语句列表中多了 return 语句能够用。
// 普通函数体
function foo(){
//Function body
}
// 异步函数体
async function foo() {
// todo
}
// 生成器函数体
function *foo() {
....
}
// 异步生成器函数体
async function *foo() {
...
}
复制代码
var 声明永远做用于脚本、模块和函数体这个级别,在预处理阶段,不关心赋值的部分,只管在当前做用域声明这个变量。
var 会穿透一切语句结构如 for if
function 声明出如今 if 等语句中的状况有点复杂,它仍然做用于脚本、模块和函数体级别,在预处理阶段,仍然会产生变量,它再也不被提早赋值:
console.log(foo);
if(true) {
function foo(){ }
}
// undefined
复制代码
class 的声明做用不会穿透 if 等语句结构,因此只有写在全局环境才会有声明做用
脚本和模块都支持一种特别的语法,叫作指令序言(Directive Prologs)。
这里的指令序言最先是为了 use strict 设计的,它规定了一种给 JavaScript 代码添加元信息的方式。
"use strict"; // 指令序言
function f(){
console.log(this);
};
f.call(null); // null
复制代码
while , do while
for in 循环
for in 循环枚举对象的属性,这里体现了属性的 enumerable 特征。
let o = { a: 10, b: 20}
Object.defineProperty(o, "c", {enumerable:false, value:30})
for(let p in o)
console.log(p);
复制代码
for of 循环和 for await of 循环
iterator 机制,咱们能够给任何一个对象添加 iterator,使它能够用于 for of 语句
let o = {
[Symbol.iterator]:() => ({
_value: 0, next(){
if(this._value == 10) return { done: true }
else return { value: this._value++, done: false };
}
})
}
for(let e of o) console.log(e);// 0 1 2 3 4 5 6 7 8 9
复制代码
generator function 定义iterator
function* foo() {
yield 0;
yield 1;
yield 2;
yield 3;
}
for(let e of foo())
console.log(e) // 0 1 2 3
复制代码
异步生成器函数
表达式语句实际上就是一个表达式,它是由运算符链接变量或者直接量构成的
表达式的原子项:Primary Expression。它是表达式的最小单位,它所涉及的语法结构也是优先级最高的。
Primary Expression 包含了各类“直接量”,直接量就是直接用某种语法写出来的具备特定类型的值。咱们已经知道,在运行时有各类值,好比数字 123,字符串 Hello world,因此通俗地讲,直接量就是在代码中把它们写出来的语法。
任何表达式加上圆括号,都被认为是 Primary Expression,这个机制使得圆括号成为改变运算优先顺序的手段。
( a + b )
复制代码
Member Expression 一般是用于访问对象成员的。它有几种形式:
a.b;
a["b"];
new.target;
super.b;
复制代码
Member Expression 最初设计是为了属性访问的,不过从语法结构须要,如下两种在 JavaScript 标准中当作 Member Expression:
f`a${b}c`;
复制代码
这是一个是带函数的模板,这个带函数名的模板表示把模板的各个部分算好后传递给一个函数。
new Cls();
复制代码
另外一个是带参数列表的 new 运算,注意,不带参数列表的 new 运算优先级更低,不属于 Member Expression。
这种很是简单,Member Expression 加上 new 就是 New Expression(固然,不加 new 也能够构成 New Expression,JavaScript 中默认独立的高优先级表达式均可以构成低优先级表达式)。
除了 New Expression,Member Expression 还能构成 Call Expression。它的基本形式是 Member Expression 后加一个括号里的参数列表,或者咱们能够用上 super 关键字代替 Member Expression。
a.b(c);
super();
a.b(c)(d)(e);
a.b(c)[3];
a.b(c).d;
a.b(c)`xyz`;
复制代码
New Expression 和 Call Expression 统称 LeftHandSideExpression,左值表达式。
左值表达式就是能够放到等号左边的表达式。
a() = b
复制代码
这样的用法实际上是符合语法的,只是,原生的 JavaScript 函数,返回的值都不能被赋值。所以多数时候,咱们看到的赋值将会是 Call Expression 的其它形式,如:
a().c = b;
复制代码
左值表达式最经典的用法是用于构成赋值表达式。
AssignmentExpression 赋值表达式也有多种形态,最基本的固然是使用等号赋值:
a = b
复制代码
这里须要理解的一个稍微复杂的概念是,这个等号是能够嵌套的:
a = b = c = d
复制代码
这样的连续赋值,是右结合的,它等价于下面这种:
a = (b = (c = d))
复制代码
赋值表达式能够构成 Expression 表达式的一部分。在 JavaScript 中,表达式就是用逗号运算符链接的赋值表达式。
在 JavaScript 中,比赋值运算优先级更低的就是逗号运算符了。咱们能够把逗号能够理解为一种小型的分号。
a = b, b = 1, null;
复制代码
逗号分隔的表达式会顺次执行,就像不一样的表达式语句同样。“整个表达式的结果”就是“最后一个逗号后的表达式结果”。好比咱们文中的例子,整个“a = b, b = 1, null;”表达式的结果就是“,”后面的null。
左值表达式搭配 ++ -- 运算符,能够造成更新表达式。
-- a;
++ a;
a --;
a ++;
复制代码
更新表达式会改变一个左值表达式的值。分为先后自增,先后自减一共四种。
delete a.b;
void a;
typeof a;
- a;
~ a;
! a;
await a;
复制代码
它的特色就是一个更新表达式搭配了一个一元运算符。
乘方表达式也是由更新表达式构成的。它使用**号。
++ i ** 30
2 **30 //正确
-2 ** 30 //报错
// ** 运算为右结合
复制代码
x * 2;
10 % 2;
10 / 2;
复制代码
+
-
复制代码
移位运算把操做数看作二进制表示的整数,而后移动特定位数。因此左移 n 位至关于乘以 2 的 n 次方,右移 n 位至关于除以 2 取整 n 次。
<< 向左移位
>> 向右移位
>>> 无符号向右移位
复制代码
普通移位会保持正负数。无符号移位会把减号视为符号位 1,同时参与移位:
-1 >>> 1 // 2^31
复制代码
移位表达式能够构成关系表达式
<=
>=
<
>
instanceof
in
复制代码
a instanceof "object" == true
复制代码
位运算表达式含有三种:
条件表达式由逻辑或表达式和条件运算符构成,条件运算符又称三目运算符,它有三个部分,由两个运算符?和:配合使用。
condition ? branch1 : branch2
复制代码
算法思想:冒泡排序只会操做相邻的两个数据。每次冒泡操做都会对相邻的两个元素进行⽐较,看是否知足大小关系要求,若是不满⾜就 重复n次,完成n个数据的排序
// 冒泡排序 a表示数组, n表示数组大小
function bubbleSort(a,n) {
if(n <= 1) return
for(let i = 0;i < n; ++i) {
// 提早退出冒泡排序循环的标志位
let flag = false
for (let j = 0; j < n -i -1; ++j) {
if(a[j] > a[j+1]) { // 交换
[a[j],a[j+1]] = [a[j+1],a[j]]
flag = true
}
}
if(!flag) break // 没有数据交换,提早退出
}
}
复制代码
稳定性:冒泡过程当中只有交换才会改变两个元素的先后顺序,为了了保证算法的稳定性,当相邻两个元素大小相等时,咱们不作交换,相同 顺序不会发⽣改变,因此是稳定的排序算法。
空间复杂度: 冒泡过程只涉及相邻元素的交换操做,只须要常量量级的临时空间,因此空间复杂度是O(1),是原地排序算法。
时间复杂度:
最好状况是已经排好序,只须要1次冒泡,时间复杂度O(n),
最坏状况是倒序排列的状况,须要进行n次冒泡,时间复杂度是O(n2)
平均状况: 经过有序度和逆序度分析
冒泡排序包含2个操做: 比较和交换,每交换一次,有序度就+1,总交换次数是肯定的,便是逆序度,也就是 n*(n-1)/2
初始有序 最好状况交换次数0,最坏状况交换次数n*(n-1)/2,平均状况下交换次数取二者平均值n*(n-1)/4,而⽐较操做确定比交换次数多, ⽽复杂度上限是O(n^2),因此平均状况的时间复杂度O(n^2)
Tips: 有序度和逆序度: 对于一个不彻底有序的数组,如四、五、六、三、二、1,有序元素对为3个(4,5)、(4,6)和(五、6) 对于⼀一个彻底有序的数组,如一、二、三、四、五、6,有序度就是n*(n-1)/2
,也就是15,称为满有序度; 关于这3个概念,有一个公式:逆序度=满有序度-有序度
,排序的过程就是增长有序度,减小逆序度的过程,最后达到满有序度 )
对于⼀个有序数组,往里面添加一个新元素,遍历该数组,找到须要插入的位置,将其插入。
算法思想: 把数组中数据分为2个区间,已排序区和未排序区。初始已排序区只有1个元素,就是数组的第⼀个元素。 核⼼思想是取未排序区的元素,在已排序区中找到合适的插入位置将其插入,重复这个过程,直到未排序区的元素为空,算法结束 。
// 插入排序, a表示数组,n表示数组大小
function insertionSort(a,n) {
if(n <= 1) return
for (let i = 1;i < n; ++i) {
let value = a[i]
let j = i - 1
while(j >= 0 && a[j] > value) {
a[j+1] = a[j] // 数据迁移
j--
}
a[j+1] = value // 插入数据
}
}
复制代码
性能分析
算法思想: 思路和插⼊排序相似,也分已排序区间和未排序区间。可是选择排序每次会从未排序区间中找到最小的元素,将其放到已排序区
// 选择排序 a表示数组 n表示数组大小
function selectionSort( a, n) {
if (n <= 1) return;
for (let i = 0; i < n - 1; ++i) {
let minIndex = i;
for (let j = i + 1;j < n; ++j) {
if (a[j] < a[minIndex]) {
minIndex = j;
}
}
[a[i], a[minIndex]] = [a[minIndex], a[i]] // 交换
}
}
复制代码
性能分析:
算法思想:
算法思想: 分治。要排序⼀个数组,先把数组从中间分红先后2部分,而后对先后部分别排序,再讲排序好的两部分结合在一块儿 。
性能分析:
算法思想:
待排序数组A[p..r]
,选择p
到r
之间的任意⼀个数据做为pivot(分区点),遍历p
到r
之间的数据,将小于pivot的放在左边,⼤于pi 边,pivot放中间。数组被分红3部分,前面A[p..q-1]
都是⼩于pivot的,中间是pivot,后⾯面A[q+1..r]
都是大于pivot的。而后再根据分治 归排序A[p..q-1]
和A[q+1..r]
,直到区间缩小为1,说明排序完成。
利用递归思想
quick_sort(p..r) = quick_sort(p..q-1) + quick_sort(q+1..r)
// 终止条件;
p >= r
// 快速排序 a是数组 n表示数组大小
quick_sort(a, n){
quick_sort_c(a, 0, n-1)
}
// 递归 p r 为下标
quick_sort_c(a,p,r) {
if (p >= r) return;
q = partition(a, p, r)
quick_sort_c(a, p ,q-1)
quick_sort_c(a,q+1,r)
}
复制代码
partition函数实现思路
随机选择一个元素做为pivot(通常状况下,能够选择p到r区间的最后一个元素),而后对A[p...r]
分区,函数返回pivot下标。若是不考虑内容消耗,能够临时申请2个数组x和y来帮助完成partition功能,只须要遍历A[p...r]
,把小于pivot的元素拷贝到数组x,把大于pivot的元素拷贝到s数组y,最后再把x中元素,pivot元素,y中元素按顺序拷贝到A[p...r]
便可。注意这时排序就再也不是原地排序了。
为了了让partition函数不占用额外的内存空间,咱们须要在A[p..r]
的原地完成分区操做,具体方法以下
处理相似选择排序,假设pivot=A[r]
,经过游标i
把A[p..r-1]
分红2部分,A[p..i-1]
中的元素都⼩于pivot,叫它"已处理理区间",把A[i...r-1]
为"未处理理区间"。每次从未处理理区间A[i..r-1]
中取⼀个元素A[j]
,与pivot对比,若是⼩于pivot,则将其加⼊到已处理区间的末尾,也就是A[i]
数组中插⼊数据致使的数据搬移,选择将A[i]
和A[j]
交换,这样就能够保证O(1)时间复杂度下内将A[j]
放到下标为i的位置。
性能分析:
空间复杂度O(1) 原地排序
稳定性: 非稳定排序
时间复杂度:O(nlongn)