引子:此文很多语言是基于自身的理解,语言尽量精炼,如有概念性错误,欢迎指正。
1.变量类型和计算
JS数据类型
字符串、数字、布尔、undefined、null、大整数、符号、对象
tring、number、boolean、undefined、null、bigint、symbol、object
值类型和引用类型的区别
只有 object 是引用类型哦,也叫复杂数据类型
- 基本数据类型直接按值存在栈中,基础数据类型赋值时给值
- 引用数据类型的数据存在堆内存中,但是数据指针是存放在栈内存中,访问引用数据时,先从栈内存中获取指针,通过指针在堆内存中找到数据,引用数据类型赋值时给地址
typeof
- 识别所有值类型
numberstringbooleanundefined新增的Symbol和bigInt,除了null是Object(bug) - 识别函数 funcition
- 识别数组对象都为 Object
instanceof
- instanceof 运算符可以用来检测“某对象是不是某个类的实例”
- 也可以判断引用数据类型 Array、Object 和 Function,但是Number,Boolean,String 基本数据类型不能判断
Object.prototype.toString.call()
- 优点:基本和引用数据都能判断
- 缺点:写法繁琐不容易记,推荐进行封装后使用
function typeOf(data) {
return Object.prototype.toString.call(data).slice(8, -1);
}
== 运算符
何时使用 === 何时使用 ==
== 不严格比较两边的类型是否相同
字符串拼接
truly 和 falsely 变量
- truly 变量:
!!a === true或者 Boolean(a)为 true 的变量 - falsely变量:
!!a === false或者 Boolean(a)为false 的变量
知晓上面有利于判断 if,switch 等条件语句和逻辑判断 & || 的的执行流程
- if(truly变量){执行语句} if(falsely变量) {不执行该语句}
逻辑判断 && 前面为真继续往后走
逻辑判断 || 前面为假继续往后走
!相当于对当前值转换为布尔值后取反
深拷贝
- 注意判断值类型和引用类型
- 注意判断是数组还是对象
- 递归
2.作用域链和闭包和this
作用域链
JS的作用域有全局、函数、块级以及严格模式下的 eval 作用域
当前作用域没有定义的变量,这成为 自由变量
当前作用域如果获取该自由变量,就需要向父级作用域一层层向外寻找至全局作用域,这一层层就是作用域链
**注意:**自由变量的寻找, 依据的是函数定义时的作用域链,而不是函数执行时的哦
闭包
理解:读取外层函数内部的变量并且让这些变量始终保持在内存之中
换一种通俗的说法:一个函数中嵌套另一个函数,内部函数使用了外部函数的参数或变量,构成闭包
经典使用闭包解决题目:
<ul>
<li>编号1,点击我请弹出1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>
<script>
// 点击 li 按顺序打印 1 2 3 4 5
var list = document.getElementsByTagName('li');
for (var i = 0; i < list.length; i++) {
list[i].addEventListener('click', function(i){
return function(){
console.log(i + 1)
}
}(i))
}
</script>
闭包的场景:
- 函数作为返回值
- 函数作为参数传递
利用闭包的特性我们就可以在模拟私有变量的情况下缓存操作内部数据
this
函数的上下文 this 需要调用的时候决定
| 规则 | 上下文 |
|---|---|
| 函数() | window(严格模式undefined) |
| 对象.函数() | 对象 |
| 数组[下标]() | 数组 |
| DOM事件处理函数 | 绑定的DOM元素 |
| call、apply、bind | 任意指定 |
| IIFE | window |
| 定时器 | window |
| new调用的函数 | 秘密创建出的对象 |
| 箭头函数 | 上层作用域 |
手写call、apply、bind
3.原型和原型链
原型链
这题复杂,但是我们可以从具体化实例入手一步步解释:
每个构造函数都有显示原型 prototype
每个实例都有隐式原型 _proto_ (方便叫法和写法,勿cue)
构造函数.prototype === 实例的隐式原型 _proto_
当获取对象属性 yunmu.name 或执行方法 yunmu.sayhi() 时先自身寻找,找不到则会去自身 __proto__寻找
实例可以打点访问它的原型的属性和方法,这被称为“原型链查找”
假设有一个数组 arr = [] ,实际上此数组一定是 new Array() 实例化出来的,带入上面模型:
Array.prototype === arr.__proto__
此时我们可以说 arr 的原型是 Array.prototype,这上面有很多方法,根据“原型链查找”规则,arr 就可以直接调用这些方法,例如 arr.push("yunmu")
但是接下来的事情有趣了
Array.prototype 是一个对象,他实际上根本也是 new Object 实例化出来的,所以:
Object.prototype === Array.prototype__proto__
这样的话,arr 就有两层原型:
- arr 的 原型 是 Array.prototype
- arr 的原型的原型是 Object.prototype
于是一个链条实际上形成了:
arr ===> Array.prototype ===> Object.prototype
图示的话就是:
原型链的好处毋庸置疑:JS任何数组都可以直接访问统一定义在原型链上面的属性和push、toString方法,真是妙不可言
有点继承内味了,但是不支持私有属性
实现继承
ES5寄生组合继承
ES6类继承
- class本质是语法糖、继承依旧是基于原型实现的
new 做了什么
- 创建临时对象,继承构造函数 prototype 属性
- 执行构造函数,使函数上下文this 指向临时对象
- 默认返回临时对象
4.异步和单线程
JS是一门单线程语言,即同一时间只能做一件事情
浏览器和 nodejs 已支持 JS 启动进程,如 Web Worker,但有诸多限制:无法读取主线程DOM,弹窗,无法访问本地文件,只能通过消息跟主线程通信
这也很好理解,如果多处同时执行,我 html 在生成结构,JS 又在修改结构,我听谁的呢?
正是由于这种性质我们才更需要异步的机制,因为如果一个 Ajax 请求较慢,使用同步在请求期间就什么都干不了,如果使用异步,我们就可以先做别的事情,有结果了通知即可,所以有一个结论:
- 异步不会阻塞代码执行
- 同步会阻塞代码执行
异步场景
- 定时任务:
setTimeoutsetInverval - 网络请求,如
Ajax图片加载
// 小试牛刀
console.log(1); // 1
setTimeout(function () {
console.log(2); // 5
}, 1000);
console.log(3); // 2
setTimeout(function () {
console.log(4); // 4
}, 0);
console.log(5); // 3
EventLoop
浏览器的EventLoop
- 同步代码放在 call stack 里面一行行执行
- 遇到异步(定时,网络请求)记录
- 同步代码执行完先执行微任务
- 尝试 DOM 渲染
- Event Loop 开始工作,轮询查找 Callback Queue 执行宏任务
宏任务:setTimeout 、setInterval 、Ajax、requestAnimationFrame、DOM事件
微任务:Promise 、 async/await、queueMicrotask
Promise顺序的面试题
第一题:
第二题:
第三题:
Promise和setTimeout的顺序
async/await 的顺序问题
Node的EventLoop
六个阶段:
- Timers:执行 setTimeout 以及 setInterval 的回调
- Pending callbacks:执行与操作系统相关的回调函数,比如启动服务器端应用时监听端口操作的回调函数就在这里调用
- Idle, prepare:闲置阶段 - node 内部使用
- IO Poll:执行 poll 中的 I/O 队列,检查定时器是否到时间
- Check:存储 setImmediate API 的回调函数
- Closing callbacks:执行与关闭事件相关的回调,例如关闭数据库连接的回调函数等
循环体会不断运行以检测是否存在没有调用的回调函数,事件循环机制会按照先进先出的方式执行他们直到队列为空
宏任务:setInterval、setTimeout、 setImmediate
微任务:Promise.then、 async/await、Promise.catch、 Promise.finally、process.nextTick
当微任务事件队列中存在可以执行的回调函数时,会先执行微任务再进入下一个阶段执行回调函数
process.nextTick() 此方法的回调函数优先级最高,会在事件循环之前被调用
node事件循环的过程:
- 执行同步代码
- 执行
process.nextTick和微任务(前者优先级更高) - 按照顺序执行 6 个类型的宏任务(每当开始之前都执行当前的微任务)
- ...
浏览器和Nodejs事件循环的区别
- 事件循环的大概模式相同
- 宏任务有优先级区分
process.nextTick在微任务的优先级更高
但是,process.nextTick 在最新版 nodejs 中不被推荐使用,推荐使用 setImmediate
原因在于 process.nextTick 是在当前帧结束后立即执行,会阻断IO并且有最大数量限制(递归时会有问题)
而 setImmediate 不会阻断 IO ,更像是 setTimeout(fun, 0)
5.严格模式
- Javascript 设计之初,有很多不合理、不严谨、不安全之处,所有就有了严格模式避免潜在隐患
- 一般情况下,开发环境用 ES 或者 Typescript ,打包出的 js 代码使用严格模式
开启严格模式
代码(或一个函数)一开始插入一行 'use strict' 即可开启严格模式
'use strict' // 全局开启
function fn() {
'use strict' // 某个函数开启
}
严格模式的特点
- 全局变量必须声明
- 函数参数不能重名
- 禁止使用 with
- 创建 eval 作用域
- 禁止 this 指向全局作用域
'use strict'
n = 10 // ReferenceError: n is not defined
// Uncaught SyntaxError: Duplicate parameter name not allowed in this context
function fn(x, x, y) {
return
}
var obj = { x: 10 }
with (obj) {
// Uncaught SyntaxError: Strict mode code may not include a with statement
console.log(x)
}
var x = 10
eval('var x = 20; console.log(x)')
console.log(x) // 10
function fn() {
console.log('this', this) // undefined
}
fn()