前言
无论你是已经在工作岗位上亦或是正在寻求新的工作机会,掌握常见的 JavaScript 面试知识点,不但可以帮助你扩展知识面、培养逻辑思维,还能帮助你提升自己的工作能力。不妨看看,也许有你所不了解的。
tips:本文篇幅较长,建议放进夹子,反复阅读,也许每次都有不一样的收获。
正文
1、JavaScript 的数据类型及其存储方式
截止目前,JavaScript 一共有8种数据类型,可大致分为基本数据类型(也叫原始数据类型)与引用数据类型。
基本数据类型:Undefined、Null、Boolean、Number、String、Symbol(es6新增,表示独一无二的值)和 BigInt(es10新增);
引用数据类型:Object(Object本质上是由一组无序的键值对组成),里面包含 Function、Array、Date 等。
JavaScript 不支持任何创建自定义类型的机制,而所有值最终都将是上述 8 种数据类型之一
存储方式
基本数据类型:直接存储在栈(stack)中,占据空间小、大小固定、频繁使用,所以放入栈中存储;
引用数据类型:同时存储在栈(stack)和堆(heap)中,占据空间大、大小不固定。引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。
2、&&、||、!!
**&&**:逻辑与,在其操作数中找到第一个虚值表达式并返回它,如果没有找到任何虚值表达式,则返回最后一个真值表达式。它采用短路来防止不必要的工作
let n = false && 1; // false
let m = true && 1; // 1
**||**:逻辑或,在其操作数中找到第一个真值表达式并返回它。这也采用短路来防止不必要的工作。在支持 ES6 默认函数参数之前,它用于初始化函数中的默认参数值
let n = false || 1; // 1
let m = true || 1; // true
function showName(name){
let newName = name || 'leo';
console.log(newName);
}
showName(); // leo
showName('ggj'); // ggj
**!!**:将右侧的值强制转换为布尔值,这也是将值转换为布尔值的一种简单方法
let flag1 = !!1; // true
let flag2 = !!0; // false
3、JavaScript 数据类型转换
在 JavaScript 中类型转换有三种情况,分别是:
- 转换为布尔值(调用 Boolean() 方法)
- 转换为数字(调用 Number()、parseInt() 和 parseFloat() 方法)
- 转换为字符串(调用 .toString() 或者 String() 方法)
null 和 underfined 没有 .toString()
此外还有一些操作符存在隐式转换,此处不展开,可自行了解。
4、JavaScript 数据类型判断
JavaScript 中判断数据类型的方式:typeof、instanceof、constructor、**Object.prototype.toString.call()**。
typeof
typeof 对于基本数据类型来说,除了 null 都可以显示正确的类型
console.log(typeof 2); // number
console.log(typeof true); // boolean
console.log(typeof 'str'); // 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 对于对象来说,除了函数都会显示 object,所以说 typeof 并不能准确判断变量到底是什么类型。进而想判断一个对象的正确类型,这时候可以考虑使用 instanceof。
instanceof
instanceof 可以正确的判断对象的类型,因为内部机制是通过判断对象的原型链中是不是能找到类型的 prototype
console.log(2 instanceof Number); // false
console.log(true instanceof Boolean); // false
console.log('str' instanceof String); // false
console.log([] instanceof Array); // true
console.log(function(){} instanceof Function); // true
console.log({} instanceof Object); // true
instanceof 可以精准判断引用数据类型(Array,Function,Object),而基本数据类型不能被instanceof 精准判断。
instanceof 在MDN中的解释:instanceof 运算符用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性。其意思就是判断对象是否是某一数据类型(如Array)的实例,请重点关注一下是判断一个对象是否是数据类型的实例。在这里字面量值,2, true ,'str'不是实例,所以判断值为false。
constructor
console.log((2).constructor === Number); // true
console.log((true).constructor === Boolean); // true
console.log(('str').constructor === String); // true
console.log(([]).constructor === Array); // true
console.log((function() {}).constructor === Function); // true
console.log(({}).constructor === Object); // true
如果创建一个对象,更改它的原型,constructor就会变得不可靠了。
function fn(){};
fn.prototype = new Array();
let f = new fn();
console.log(f.constructor === fn); // false
console.log(f.constructor === Array); // true
Object.prototype.toString.call()
Object 对象的原型方法 toString ,使用 call 改变 this 指向,借用 Object 的 toString 方法
let a = Object.prototype.toString;
console.log(a.call(2)); // [object Number]
console.log(a.call(true)); // [object Boolean]
console.log(a.call('str')); // [object String]
console.log(a.call([])); // [object Array]
console.log(a.call(function(){})); // [object Function]
console.log(a.call({})); // [object Object]
console.log(a.call(undefined)); // [object Undefined]
console.log(a.call(null)); // [object Null]
5、null 和 undefined 的区别
首先 Undefined 和 Null 都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 undefined 和 null。
undefined 代表的含义是未定义, null 代表的含义是空对象。一般变量声明了但还没有定义的时候会返回 undefined,null 主要用于赋值给一些可能会返回对象的变量,作为初始化。
null 不是对象,虽然 typeof null 会输出 object,但是这只是 JavaScript 存在的一个悠久 Bug。在 JavaScript 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象,然而 null 表示为全零,所以将它错误的判断为 object 。
undefined 在 JavaScript 中不是一个保留字,这意味着我们可以使用 undefined 来作为一个变量名,这样的做法是非常危险的,它会影响我们对 undefined 值的判断。但是我们可以通过一些方法获得安全的 undefined 值,比如说 void 0。
当我们对两种类型使用 typeof 进行判断的时候,Null 类型化会返回 object,这是一个历史遗留的问题。当我们使用 == 对两种类型的值进行比较时会返回 true,使用 === 时会返回 false。
6、Javascript 的作用域和作用域链
作用域:作用域是定义变量的区域,它有一套访问变量的规则,这套规则来管理浏览器引擎如何在当前作用域以及嵌套的作用域中根据变量(标识符)进行变量查找。
作用域链:作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,我们可以访问到外层环境的变量和函数。
作用域链的本质上是一个指向变量对象的指针列表。变量对象是一个包含了执行环境中所有变量和函数的对象。作用域链的前端始终都是当前执行上下文的变量对象。全局执行上下文的变量对象(也就是全局对象)始终是作用域链的最后一个对象。
当我们查找一个变量时,如果当前执行环境中没有找到,我们可以沿着作用域链向后查找。
7、JavaScript 创建对象的几种方式
我们一般使用字面量的形式直接创建对象,但是这种创建方式对于创建大量相似对象的时候,会产生大量的重复代码。但 JavaScript
和一般的面向对象的语言不同,在 ES6 之前它没有类的概念。但是我们可以使用函数来进行模拟,从而产生出可复用的对象。
详细内容请参考
JavaScript 创建对象的几种方式_Lea-CSDN博客_javascript创建对象的几种方式
8、JavaScript 实现继承的几种方式
1、原型链继承
2、使用借用构造函数继承
3、组合继承
4、原型式继承
5、寄生式继承
6、寄生式组合继承
详细内容请参考
JavaScript实现继承的几种方式_刘欢的博客-CSDN博客_javascript继承方式
9、this、call、apply、bind
1、在浏览器里,在全局范围内 this 指向 window 对象;
2、在函数中,this 永远指向最后调用他的那个对象;
3、构造函数中,this 指向 new 出来的那个新的对象;
4、call、apply、bind 中的 this 被强绑定在指定的那个对象上;
5、箭头函数中 this 比较特殊,箭头函数中的 this 为父作用域的 this,不是调用时的 this。前四种方式都是调用时确定,也就是动态的,而箭头函数的 this 指向是静态的,声明的时候确定;
6、apply、call、bind 都是 JavaScript 给函数内置的一些API,调用他们可以为函数指定 this 的执行,同时也可以传参
关于手写实现 call、apply、bind,请参考本人博客
Js面试常客之手写实现call、apply、bind方法(一看就会,附详细截图)_前端不释卷leo的博客-CSDN博客我们知道,call、apply、bind函数可以改变this的指向。用例子验证,现在有一个对象,里面有属性跟一个showName方法let initObj = { name:'gao', age:18 showName:function(){ console.log(this.name,'name') }}直接执行如下代码initObj.showName()打印信息如下此时,新建一个对象,里面只有属性,没有方法let newObj = { .https://blog.csdn.net/qq_41809113/article/details/121364961
10、JavaScript 的原型与原型链
详细内容请参考本人博客
下班前几分钟,我彻底弄懂了JavaScript的原型与原型链_前端不释卷leo的博客-CSDN博客前言JavaScript 原型与原型链历来都是面试的重点,也是难点,理解起来没有那么容易。正文理解原型的几个要点,能更容易理解原型这个概念:1、所有的引用类型(数组、对象、函数)可以自由扩展属性(除null以外);2、所有的引用类型都有一个“__proto__”属性(隐式原型,是一个对象);3、所有的函数都有一个“prototype”属性(显式原型,是一个对象);4、所有引用类型的“__proto__”属性指向它的构造函数的“prototype”属性;5、当访问一个对象的属性https://blog.csdn.net/qq_41809113/article/details/122720550
11、JavaScript 的闭包
闭包是指有权访问另一个函数作用域内变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以 访问到当前函数的局部变量。
闭包有两个常用的用途
- 闭包的第一个用途是使我们在函数外部能够访问到函数内部的变量。通过使用闭包,我们可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。
- 函数的另一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。
function a(){
let n = 0;
return function add(){
n++;
console.log(n);
}
}
let a1 = a(); // 注意,函数名只是一个标识(指向函数的指针),而()才是执行函数;
a1(); // 1
a1(); // 2 第二次调用n变量还在内存中
其实闭包的本质就是作用域链的一个特殊的应用,只要了解了作用域链的创建过程,就能够理解闭包的实现原理。
12、DOM 与 BOM
DOM:指的是文档对象模型,它指的是把文档当做一个对象来对待,这个对象主要定义了处理网页内容的方法和接口。
BOM:指的是浏览器对象模型,它指的是把浏览器当做一个对象来对待,这个对象主要定义了与浏览器进行交互的法和接口。BOM 的核心是 window,而 window 对象具有双重角色,它既是通过 JavaScript 访问浏览器窗口的一个接口,又是一个 Global(全局) 对象。这意味着在网页中定义的任何对象,变量和函数,都作为全局对象的一个属性或者方法存在。window 对象含有 location 对象、navigator 对象、screen 对象等子对象,并且 DOM 的最根本的对象 document 对象也是 BOM 的 window 对 象的子对象。
详细内容请参考
Javascript操作BOM和DOM详解(1)_葡萄藤的博客-CSDN博客_dom和bom操作
13、事件传播
当事件发生在DOM元素上时,该事件并不完全发生在那个元素上。在“当事件发生在DOM元素上时,该事件并不完全发生在那个元素上。
事件传播有三个阶段:
1、捕获阶段–事件从 window 开始,然后向下到每个元素,直到到达目标元素事件或event.target。
2、目标阶段–事件已达到目标元素。
3、冒泡阶段–事件从目标元素冒泡,然后上升到每个元素,直到到达 window。
14、事件捕获
当事件发生在 DOM 元素上时,该事件并不完全发生在那个元素上。在捕获阶段,事件从 window 开始,一直到触发事件的元素。
window----> document----> html----> body \---->目标元素
假设有如下的 HTML 结构
<div class="grandparent">
<div class="parent">
<div class="child">1</div>
</div>
</div>
对应的 JavaScript 代码
function addEvent(el, event, callback, isCapture = false) {
if (!el || !event || !callback || typeof callback !== 'function') return;
if (typeof el === 'string') {
el = document.querySelector(el);
};
el.addEventListener(event, callback, isCapture);
}
addEvent(document, 'DOMContentLoaded', () => {
const child = document.querySelector('.child');
const parent = document.querySelector('.parent');
const grandparent = document.querySelector('.grandparent');
addEvent(child, 'click', function (e) {
console.log('child');
});
addEvent(parent, 'click', function (e) {
console.log('parent');
});
addEvent(grandparent, 'click', function (e) {
console.log('grandparent');
});
addEvent(document, 'click', function (e) {
console.log('document');
});
addEvent('html', 'click', function (e) {
console.log('html');
})
addEvent(window, 'click', function (e) {
console.log('window');
})
});
addEventListener
方法具有第三个可选参数
useCapture
,其默认值为 false,事件将在冒泡阶段中发生,如果为 true,则事件将在捕获阶段中发生。如果单击 child 元素,它将分别在控制台上打印
window
,
document
,
html
,
grandparent
和
parent
,这就是事件捕获。
15、事件冒泡
事件冒泡刚好与事件捕获相反,
当前元素---->body \----> html---->document \---->window
。当事件发生在 DOM 元素上时,该事件并不完全发生在那个元素上。在冒泡阶段,事件冒泡,或者事件发生在它的父代,祖父母,祖父母的父代,直到到达 window 为止。
假设有如下的 HTML 结构
<div class="grandparent">
<div class="parent">
<div class="child">1</div>
</div>
</div>
对应的 JavaScript 代码
function addEvent(el, event, callback, isCapture = false) {
if (!el || !event || !callback || typeof callback !== 'function') return;
if (typeof el === 'string') {
el = document.querySelector(el);
};
el.addEventListener(event, callback, isCapture);
}
addEvent(document, 'DOMContentLoaded', () => {
const child = document.querySelector('.child');
const parent = document.querySelector('.parent');
const grandparent = document.querySelector('.grandparent');
addEvent(child, 'click', function (e) {
console.log('child');
});
addEvent(parent, 'click', function (e) {
console.log('parent');
});
addEvent(grandparent, 'click', function (e) {
console.log('grandparent');
});
addEvent(document, 'click', function (e) {
console.log('document');
});
addEvent('html', 'click', function (e) {
console.log('html');
})
addEvent(window, 'click', function (e) {
console.log('window');
})
});
addEventListener
方法具有第三个可选参数
useCapture
,其默认值为 false,事件将在冒泡阶段中发生,如果为 true,则事件将在捕获阶段中发生。如果单击 child 元素,它将分别在控制台上打印
child
,
parent
,
grandparent
,
html
,
document
和
window
,这就是事件冒泡。
16、事件委托
事件委托本质上是利用了浏览器事件冒泡的机制。因为事件在冒泡过程中会上传到父节点,并且父节点可以通过事件对象获取到 目标节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件,这种方式称为事件代理。
使用事件代理我们可以不必要为每一个子元素都绑定一个监听事件,这样减少了内存上的消耗。并且使用事件代理我们还可以实现事件的动态绑定,比如说新增了一个子节点,我们并不需要单独地为它添加一个监听事件,它所发生的事件会交给父元素中的监听函数来处理。
<ul id="list">
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
......
<li>item n</li>
</ul>
// ...... 代表中间还有未知数个 li,我们可以在ul添加事件(如onclick),实现对li事件的代理
17、添加、移除、移动、复制、创建和查找节点
创建新节点
createDocumentFragment(); // 创建一个DOM片段
createElement(); // 创建一个具体的元素
createTextNode(); // 创建一个文本节点
添加、移除、替换、插入
appendChild(node);
removeChild(node);
replaceChild(newNode,oldNode);
insertBefore(newNode,oldNode);
查找
getElementById();
getElementsByName();
getElementsByTagName();
getElementsByClassName();
querySelector();
querySelectorAll();
属性操作
getAttribute(key);
setAttribute(key, value);
hasAttribute(key);
removeAttribute(key);
18、JavaScript 数组方法与字符串方法
数组方法
详细内容请参考本人博客
2022 JavaScript 数组(Array)方法1w+字汇总(含数组新特性,全到没朋友,再也不用东拼西凑了)_前端不释卷leo的博客-CSDN博客目录前言创建数组字面量方式构造函数方式Array.of 方式Array.from 方式数组方法返回新数组concat()join()slice()map()filter()toLocaleString()toString()flat()flatMap()entries()keys()values()改变原数组push()pop()unshift()shift()sort()reverse()...https://blog.csdn.net/qq_41809113/article/details/122588160?spm=1001.2014.3001.5502
字符串方法
方法描述charAt()返回在指定位置的字符。charCodeAt()返回在指定的位置的字符的 Unicode 编码。concat()连接两个或更多字符串,并返回新的字符串。endsWith()判断当前字符串是否是以指定的子字符串结尾的(区分大小写)。fromCharCode()将 Unicode 编码转为字符。indexOf()返回某个指定的字符串值在字符串中首次出现的位置。includes()查找字符串中是否包含指定的子字符串。lastIndexOf()从后向前搜索字符串,并从起始位置(0)开始计算返回字符串最后出现的位置。match()查找找到一个或多个正则表达式的匹配。repeat()复制字符串指定次数,并将它们连接在一起返回。replace()在字符串中查找匹配的子串,并替换与正则表达式匹配的子串。replaceAll()在字符串中查找匹配的子串,并替换与正则表达式匹配的所有子串。search()查找与正则表达式相匹配的值。slice()提取字符串的片断,并在新的字符串中返回被提取的部分。split()把字符串分割为字符串数组。startsWith()查看字符串是否以指定的子字符串开头。substr()从起始索引号提取字符串中指定数目的字符。substring()提取字符串中两个指定的索引号之间的字符。toLowerCase()把字符串转换为小写。toUpperCase()把字符串转换为大写。trim()去除字符串两边的空白。toLocaleLowerCase()根据本地主机的语言环境把字符串转换为小写。toLocaleUpperCase()根据本地主机的语言环境把字符串转换为大写。valueOf()返回某个字符串对象的原始值。toString()返回一个字符串。
19、常用的正则表达式
以下仅为收集,涉及不深
// 匹配 16 进制颜色值
let color = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})/g;
// 匹配日期,如 yyyy-mm-dd 格式
let date = /^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/;
// 匹配 qq 号
let qq = /^[1-9][0-9]{4,10}$/g;
// 手机号码正则
let phone = /^1[34578]\d{9}$/g;
// 用户名正则
let username = /^[a-zA-Z\$][a-zA-Z0-9_\$]{4,16}$/;
// Email正则
let email = /^([A-Za-z0-9_\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/;
// 身份证号(18位)正则
let cP = /^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/;
// URL正则
let urlP= /^((https?|ftp|file):\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/;
// ipv4地址正则
let ipP = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
// 车牌号正则
let cPattern = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}[A-Z0-9]{4}[A-Z0-9挂学警港澳]{1}$/;
// 强密码(必须包含大小写字母和数字的组合,不能使用特殊字符,长度在8-10之间)
let pwd = /^(?=.\d)(?=.[a-z])(?=.[A-Z]).{8,10}$/
20、如何创建一个 Ajax
我对 ajax 的理解是,它是一种异步通信的方法,通过直接由 JavaScript 脚本向服务器发起 http 通信,然后根据服务器返回的数据,更新网页的相应部分,而不用刷新整个页面的一种方法。
创建步骤
手写原生 Ajax
// 创建Ajax对象
let xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP'); // 兼容IE6及以下版本
// 配置Ajax请求地址
xhr.open('get','index.xml',true); // true是异步请求,false是同步的请求,不建议
// 发送请求
xhr.send(null); // 严谨写法
// 监听请求,接收响应
xhr.onreadystatechange = function(){
if(xhr.readyState == 4 && xhr.status == 200){
console.log(xhr.responseText);
}
}
jQuery 写法
$.ajax({
type: 'post',
url: '',
async: ture, // async异步 sync同步
data: data, // 针对post请求
dataType: 'jsonp',
success: function (msg) {
},
error: function (error) {
}
});
21、JavaScript 延迟加载方式
JavaScript 的加载、解析和执行会阻塞页面的渲染过程,因此我们希望 JavaScript 脚本能够尽可能的延迟加载,提高页面的渲染速度。
有以下几种方式:
1、将 JavaScript 脚本放在文档的底部,来使 JavaScript 脚本尽可能的在最后来加载执行;
2、给 JavaScript 脚本添加 defer 属性,这个属性会让脚本的加载与文档的解析同步解析,然后在文档解析完成后再执行这个脚本文件,这样的话就能使页面的渲染不被阻塞。多个设置了 defer 属性的脚本按规范来说最后是顺序执行的,但是在一些浏览器中可能不是这样;
3、给 JavaScript 脚本添加 async 属性,这个属性会使脚本异步加载,不会阻塞页面的解析过程,但是当脚本加载完成后立即执行 JavaScript 脚本,这个时候如果文档没有解析完成的话同样会阻塞。多个 async 属性的脚本的执行顺序是不可预测的,一般不会按照代码的顺序依次执行;
4、动态创建 DOM 标签的方式,我们可以对文档的加载事件进行监听,当文档加载完成后再动态的创建 script 标签来引入 JavaScript 脚本。
22、JavaScript 模块化
JavaScript 中现在比较成熟的有四种模块加载方案
- CommonJS,它通过 require 来引入模块,通过 module.exports 定义模块的输出接口。这种模块加载方案是服务器端的解决方案,它是以同步的方式来引入模块的,因为在服务端文件都存储在本地磁盘,所以读取非常快,所以以同步的方式加载没有问题。但如果是在浏览器端,由于模块的加载是使用网络请求,因此使用异步加载的方式更加合适。
- AMD,这种方案采用异步加载的方式来加载模块,模块的加载不影响后面语句的执行,所有依赖这个模块的语句都定义在一个回调函数里,等到加载完成后再执行回调函数。require.js 实现了 AMD 规范。
- CMD,这种方案和 AMD 方案都是为了解决异步模块加载的问题,sea.js 实现了 CMD 规范。它和require.js的区别在于模块定义时对依赖的处理不同和对依赖模块的执行时机的处理不同。
- ES6 使用 import 和 export 的形式来导入导出模块。
详细内容请参考
浅谈JavaScript模块化_peppa_pig的博客-CSDN博客
23、 ES6 模块与 CommonJS 模块的差异
1、CommonJS
模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
CommonJS
模块输出的是值的,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令 import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。
2、
CommonJS
模块是运行时加载,ES6 模块是编译时输出接口。
CommonJS
模块就是对象,即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
24、JavaScript 的运行机制
单线程
JavaScript 语言的一大特点就是单线程,即同一时间只能做一件事情。
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
事件循环(event loop)
JavaScript 代码执行过程中会有很多任务,这些任务总的分成两类:
- 同步任务
- 异步任务
当我们打开网站时,网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务,如图
如上图所示:
- 同步和异步任务分别进入不同的执行“场所”,同步的进入主线程,异步的进入
Event Table
并注册函数。 - 当指定的事情完成时,
Event Table
会将这个函数移入Event Queue
。 - 主线程内的任务执行完毕为空,会去
Event Queue
读取对应的函数,进入主线程执行。 - 上述过程会不断重复,也就是常说的
Event Loop
(事件循环)。
那主线程执行栈何时为空呢?JavaScript 引擎存在
monitoring process
进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去
Event Queue
那里检查是否有等待被调用的函数。
除了同步任务和异步任务,任务还可以更加细分为 macrotask (宏任务)和 microtask (微任务),JavaScript 引擎会优先执行微任务。
微任务:promise 的回调、node 中的 process.nextTick 、对 Dom 变化监听的 MutationObserver。
宏任务: script 脚本的执行、setTimeout ,setInterval ,setImmediate 一类的定时事件,还有如 I/O 操作、UI 渲染等。
事件循环通俗表述
1、首先 JavaScript 是单线程运行的,在代码执行的时候,通过将不同函数的执行上下文压入执行栈中来保证代码的有序执行;
2、在执行同步代码的时候,如果遇到了异步事件,JavaScript 引擎并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务;
3、当同步事件执行完毕后,再将异步事件对应的回调加入到与当前执行栈中不同的另一个任务队列中等待执行;
4、任务队列可以分为宏任务对列和微任务对列,在当前执行栈中的事件执行完毕后,JavaScript 引擎首先会判断微任务对列中是否有任务可以执行,如果有就将微任务队首的事件压入栈中执行;
5、当微任务对列中的任务都执行完成后再去判断宏任务对列中的任务。
举例
setTimeout(function() {
console.log(1)
}, 0);
new Promise(function(resolve, reject) {
console.log(2);
resolve()
}).then(function() {
console.log(3)
});
process.nextTick(function () {
console.log(4)
})
console.log(5)
主线程开始执行 -->
遇到 setTimeout,将 setTimeout 的回调函数丢到宏任务队列中 -->
往下执行new Promise 立即执行,输出2,then 的回调函数丢到微任务队列中 -->
再继续执行,遇到 process.nextTick,同样将回调函数扔到为任务队列 -->
再继续执行,输出5 -->
当所有同步任务执行完成后看有没有可以执行的微任务,发现有 then 函数和 nextTick 两个微任务,先执行哪个呢?process.nextTick 指定的异步任务总是发生在所有异步任务之前,因此先执行process.nextTick,输出4 -->
然后执行 then 函数,输出3,第一轮执行结束。
从宏任务队列开始,发现 setTimeout 回调,输出1 -->
执行完毕,因此结果是 25431 。
25、arguments 对象
arguments 对象是函数中传递的参数值的集合。它是一个类似数组的对象,因为它有一个 length 属性,我们可以使用数组索引表示法 arguments[1] 来访问单个值,但它没有数组中的内置方法,如:forEach、reduce、filter 和 map。
我们可以使用 Array.prototype.slice 将 arguments 对象转换成一个数组
function fn() {
return Array.prototype.slice.call(arguments);
}
箭头函数中没有 arguments 对象。
let fn = () => arguments;
fn(); // Throws an error - arguments is not defined
当我们调用函数four时,它会抛出一个
ReferenceError: arguments is not defined error
。使用rest 语法,可以解决这个问题
let fn = (...args) => args;
这会自动将所有参数值放入数组中。
26、无意的全局变量
举例
function fn() {
let a = b = 0;
}
fn();
为什么在调用这个函数时,代码中的 b 会变成一个全局变量?
原因是赋值运算符是从右到左的求值的。这意味着当多个赋值运算符出现在一个表达式中时,它们是从右向左求值的。即上面的代码实际上是这样
function fn() {
let a = (b = 0);
}
fn();
首先,表达式 b = 0 求值,在本例中 b 没有声明。因此,JavaScript 引擎在这个函数外创建了一个全局变量 b,之后表达式 b = 0 的返回值为 0,并赋给新的局部变量 a。
我们可以通过在赋值之前先声明变量来解决这个问题
function fn() {
let a,b;
a = b = 0;
}
fn();
27、内存泄漏
造成内存泄漏可能操作
- 意外的全局变量
- 被遗忘的计时器或回调函数
- 脱离 DOM 的引用
- 闭包
对于内存泄露,我单独写了一篇博客,介绍了内存泄漏相关概念、内存泄漏的原因以及如何避免内存泄漏,详细内容请参考
识别并避免 Js 内存泄漏,跟低级缺陷say goodbye,让老总对你刮目相看_前端不释卷leo的博客-CSDN博客目录内存泄漏常见的内存泄漏类型1、意外的全局变量2、被遗忘的定时器或回调函数3、脱离DOM的引用4、闭包扩展垃圾回收机制引用计数法标记清除法(常用)内存泄漏对于持续运行的服务进程(daemon),必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。 对于不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)。常见的内存泄漏类型1、意外的全局变量在一个局部作用域中,未定义的变量会在全局对象创建一个新https://blog.csdn.net/qq_41809113/article/details/122285947
28、ECMAScript 2015(ES6)新特性
- 块作用域
- 类
- 箭头函数
- 模板字符串
- 加强的对象字面
- 对象解构
- Promise
- 模块
- Symbol
- 代理(proxy)、Set
- 函数默认参数
- rest 和 扩展运算符...
ES6 及其以上的新特性我写了不止一篇博客对它们进行总结,详细内容请前往本人专栏“js and ts”阅读,相信你会有不一样的收获。
29、var、let、const
var 声明的变量会挂载在 window 上,而 let 和 const 声明的变量不会
举例
var a = 100;
console.log(a,window.a); // 100 100
let b = 10;
console.log(b,window.b); // 10 undefined
const c = 1;
console.log(c,window.c); // 1 undefined
var 声明变量存在变量提升,let 和 const 不存在变量提升
举例
console.log(a); // undefined => a已声明还没赋值,默认得到undefined值
var a = 100;
console.log(b); // 报错:b is not defined => 找不到b这个变量
let b = 10;
console.log(c); // 报错:c is not defined => 找不到c这个变量
const c = 10;
let 和 const 声明形成块作用域
举例
if(1){
var a = 100;
let b = 10;
}
console.log(a); // 100 全局作用域
console.log(b); // 报错:b is not defined => 找不到b这个变量 块作用域
if(1){
var a = 100;
const c = 1;
}
console.log(a); // 100 全局作用域
console.log(c); // 报错:c is not defined => 找不到c这个变量 块作用域
同一作用域下 let 和 const 不能声明同名变量,而 var 可以
举例
var a = 100;
console.log(a); // 100
var a = 10;
console.log(a); // 10
let b = 100;
let b = 10;
// 控制台报错:Identifier 'b' has already been declared => 标识符b已经被声明了。
暂存死区
var a = 100;
if(1){
a = 10;
// 在当前块作用域中存在a使用let/const声明的情况下,给a赋值10时,只会在当前作用域找变量a,
// 而这时,还未到声明时候,所以控制台Error:a is not defined
let a = 1;
}
const 使用
/*
* 1、一旦声明必须赋值,不能使用null占位。
*
* 2、声明后不能再修改
*
* 3、如果声明的是复合类型数据,可以修改其属性
*
* */
const a = 100;
a = 10; // 报错
const list = [];
list[0] = 10;
console.log(list); // [10]
const obj = {a:100};
obj.name = 'apple';
obj.a = 10000;
console.log(obj); // {a:10000,name:'apple'}
30、箭头函数
箭头函数表达式的语法比函数表达式更简洁,并且没有自己的
this,arguments,super或new.target
。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。
// ES5 Version
function greet(name) {
return 'Hello ' + name + '!';
}
// ES6 Version
let greet = (name) => `Hello ${name}`;
let greet2 = name => `Hello ${name}`;
详细内容请参考本人博客
Js 箭头函数 详细介绍(多种使用场景差异,你学会了吗?)_前端不释卷leo的博客-CSDN博客_js 箭头函数简要认识箭头函数是在ES6中添加的一种规范,简化了匿名函数定义的写法。基本格式完整写法let fn = (x,y) => { return x + y;}//function()写法let fn = function(x,y) { return x + y;}只有1个参数时,可以省略 ()//参数只有1个时,可以省略 ()let fn = x => { return x + x;}//function()写法let fn = functhttps://blog.csdn.net/qq_41809113/article/details/122003373
31、模板字符串
模板字符串是在 JavaScript 中创建字符串的一种新方法。
我们可以通过使用反引号使模板字符串化
// ES5 Version
var greet = 'Hi I\'m leo';
// ES6 Version
let greet = `Hi I'm leo`;
在 ES5 中我们需要使用一些转义字符来达到多行的效果,在模板字符串不需要这么麻烦
// ES5 Version
var str = '\n'
+ ' I \n'
+ ' Am \n'
+ 'Iron Man \n';
// ES6 Version
let str = `
I
Am
Iron Man
`;
在 ES5 版本中,我们需要添加 \n 以在字符串中添加新行。在模板字符串中,我们不需要这样做
// ES5 Version
function greet(name) {
return 'Hello ' + name + '!';
}
// ES6 Version
function greet(name) {
return `Hello ${name} !`;
}
在 ES5 版本中,如果需要在字符串中添加表达式或值,则需要使用 + 运算符。在模板字符串中,我们可以使用
${expr}
嵌入一个表达式,这使其比 ES5 版本更整洁。
模板字符串在开发中经常使用,更多关于它的知识可以参考本人的其他 JavaScript 博客。
32、解构赋值
对象解构是从对象或数组中获取或提取值的一种新的、更简洁的方法。
假设有如下的对象
let employee = {
firstName: "leo",
lastName: "ggj",
position: "Software Developer",
age: 18
};
从对象获取属性,早期方法是创建一个与对象属性同名的变量。这种方法很麻烦,因为我们要为每个属性创建一个新变量。假设我们有一个对象,它有很多属性和方法,用这种方法提取属性会很麻烦,形如
let firstName = employee.firstName;
let lastName = employee.lastName;
let position = employee.position;
let age = employee.age;
使用解构方式语法就变得简洁多了
let { firstName, lastName, position, age } = employee;
我们还可以为属性取别名
let { firstName: fName, lastName: lName, position, age } = employee;
当然如果属性值为 undefined 时,我们还可以指定默认值
let { firstName = "leo", lastName: lName, position, age } = employee;
33、Set 对象及其用法
Set 对象允许你存储任何类型的唯一值,无论是原始值或者是对象引用。
我们可以使用 Set 构造函数创建 Set 实例
let set1 = new Set();
let set2 = new Set(["l","e","o"]);
我们可以使用 add 方法向 Set 实例中添加一个新值,因为 add 方法返回 Set 对象,所以我们可以以链式的方式再次使用 add。如果一个值已经存在于 Set 对象中,那么它将不再被添加
set2.add("f");
set2.add("g").add("h").add("i").add("j").add("k").add("k");
// 最后一个“k”不会被添加到set对象中,因为它已经存在了
我们可以使用 has 方法检查 Set 实例中是否存在特定的值
set2.has("l"); // true
set2.has("e"); // true
set2.has("m"); // false
我们可以使用 size 属性获得 Set 实例的长度
set2.size
我们可以使用 clear 方法清空 Set 中的数据
set2.clear();
我们可以使用 Set 对象来删除数组中重复的元素(常用)
let numbers = [1, 2, 3, 4, 5, 6, 6, 7, 8, 8, 5];
let uniqueNums1 = [...new Set(numbers)]; // [1,2,3,4,5,6,7,8]
let uniqueNums2 = Array.from(new Set(numbers)); // [1,2,3,4,5,6,7,8]
另外还有
WeakSet
, 与
Set
类似,也是不重复的值的集合。但是
WeakSet
的成员只能是对象,而不能是其他类型的值。
WeakSet
中的对象都是弱引用,即垃圾回收机制不考虑
WeakSet
对该对象的引用。
34、高阶函数
高阶函数只是将函数作为参数或返回值的函数
function higherOrderFunction(param,callback){
return callback(param);
}
如果你使用 react 进行开发,或许使用过高阶组件(HOC),与该思想相似。
35、深拷贝与浅拷贝
JavaScript 的深浅拷贝一直是个难点,如果现在面试官让我写一个深拷贝,我可能也只是能写出个基础版的。所以在写这条之前我拜读了收藏夹里各路大佬写的博文。具体可以看下面我贴的链接。
浅拷贝: 创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
深拷贝: 将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象。
浅拷贝实现方式
1、Object.assign() 方法:用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。
2、**Array.prototype.slice()**:slice() 返回一个新的数组对象,这一对象是一个由 begin 和 end(不包括end)决定的原数组的浅拷贝。原始数组不会被改变。
3、**拓展运算符
...
**
let obj = {
name: "leo",
flag: {
title: "better day by day",
time: "2022-02-10"
}
};
let newObj = {...obj}; // 浅拷贝
深拷贝实现方式
1、乞丐版:JSON.parse(JSON.stringify(object)),缺点诸多(会忽略 undefined、symbol、函数;不能解决循环引用;不能处理正则、new Date())
let target = JSON.parse(JSON.stringify(source));
2、基础版:浅拷贝 + 递归 (只考虑了普通的 object 和 array 两种数据类型)
function deepClone(source) {
let target = source instanceof Array ? [] : {};
for (let key in source) {
if (typeof source[key] === 'object') {
target[key] = deepClone(source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
详细内容请参考
如何写出一个惊艳面试官的深拷贝
36、模拟 new 操作符的实现
new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。new 关键字会进行如下的操作:
1、创建一个空的简单 JavaScript 对象(即{});
2、链接该对象(即设置该对象的构造函数)到另一个对象;
3、将步骤1新创建的对象作为 this 的上下文 ;
4、如果该函数没有返回对象,则返回 this。
具体实现
function Dog(name, color, age) {
this.name = name;
this.color = color;
this.age = age;
}
Dog.prototype = {
getName: function() {
return this.name
}
}
var dog = new Dog('大黄', 'yellow', 3);
由上面例子,看最后一行带 new 关键字的代码,按照上述的1,2,3,4步来解析 new 背后的操作。
第一步:创建一个简单空对象
let obj = {};
第二步:链接该对象到另一个对象(原型链)
// 设置原型链
obj.__proto__ = Dog.prototype;
第三步:将第一步新创建的对象作为 this 的上下文
// this指向obj对象
Dog.apply(obj, ['大黄', 'yellow', 3]);
第四步:如果该函数没有返回对象,则返回 this
// 因为Dog()没有返回值,所以返回obj
let dog = obj
dog.getName(); // '大黄'
如果 Dog() 有 return 则返回 return 的值
接下来我们将以上步骤封装成一个对象实例化方法,即模拟 new 的操作
function myNew(){
let obj = {};
// 取得该方法的第一个参数(并删除第一个参数),该参数是构造函数
let Constructor = [].shift.apply(arguments);
// 将新对象的内部属性__proto__指向构造函数的原型,这样新对象就可以访问原型中的属性和方法
obj.__proto__ = Constructor.prototype;
// 取得构造函数的返回值
let result = Constructor.apply(obj, arguments);
// 如果返回值是一个对象就返回该对象,否则返回构造函数的一个实例对象
return typeof result === "object" ? result : obj;
}
扩展
1、Promise、Generator、async await
详细内容请参考本人博客
Js 异步请求按顺序调用解决方案(真实工作场景,axios、Promise、async await)_前端不释卷leo的博客-CSDN博客需求背景:现在需要调用多个异步请求,要求某个请求调用成功之后,接着调另外的请求,有时候需要用上一个请求得到的结果,作为下一个请求的参数或者条件,继续调用另一个请求。演示准备:安装axiosnpm install axios --save全局使用//main.jsimport axios from 'axios'Vue.prototype.$axios = axios实现一:PromisePromise 是一个 ECMAScript 6 提供的类,目的是更加优雅地书写复.https://blog.csdn.net/qq_41809113/article/details/121086178
2、防抖、节流
详细内容请参考本人博客
Vue 防抖节流 详细介绍(面试常客、去繁从简、性能优化)_前端不释卷leo的博客-CSDN博客_vue中防抖和节流的使用场景本文主要介绍js中的防抖和节流以及在vue项目中如何使用它们来达到性能优化的目的。前置知识点:js、闭包、es6、vue等。使用背景:很多场景下,页面具有交互性,免不了会触发一些事件以及发送一些请求,依赖于像输入框、选择框、点击按钮等等。这时候就会出现一个现象,对使用者的操作不可控。如一个点击按钮,其绑定一个事件或发送一个请求,那么在连续快速点击时,如果不做限制,那么点击多次,将触发对应次数的事件或者发送对应次数的请求,这对使用者来说可能就是短时间内的简单操作,但是试想一下,在连续点击多次的情况https://blog.csdn.net/qq_41809113/article/details/121716249
3、设计模式
详细内容请参考本人博客
面试官:请简单实现一个Js单例模式(一看吓一跳,easy)_前端不释卷leo的博客-CSDN博客单例模式:顾名思义,即一个类只有一个实例,通常的做法是先判断类是否已存在实例,如果不存在,那就创建一个新的实例,如果已经存在,就直接使用已存在的实例。涉及到的知识点:JavaScript 类、闭包。新建一个简单的类 Personclass Person { constructor(name){ //构造函数,创建实例的时候执行 this.name = name } showName(){ console.log('my name is',this.name)https://blog.csdn.net/qq_41809113/article/details/121429476
更多 JavaScript 知识请阅读专栏“js and ts”的其他相关博客。
都看到这里了,一键三连走一波?关注我,一起学前端~~~
版权归原作者 前端不释卷leo 所有, 如有侵权,请联系我们删除。