一、数据类型
1、分类
- 基本数据类型:String Number Boolean Null Undefined Symbol
- 复杂数据类型/引用数据类型:Object Array Function
2、判断
1)typeof 对于基本数据类型来说,除了 null 都可以显示正确的类型
typeof 对于对象来说,除了函数都会显示 object,此方案不是很好
typeof 5; // 'number'
typeof "5"; // 'string'
typeof undefined; // 'undefined'
typeof false; // 'boolean'
typeof Symbol(); // 'symbol'
typeof null; //object
typeof NaN; //number
typeof []; // 'object'
typeof {}; // 'object'
typeof console.log; // 'function'
2)instanceof 通过原型链来判断数据类型的,但是可以篡改原型链,不完善
3)Object.prototype.toString.call() 可以检测所有的数据类型,算是一个完美的方法了
3、为什么 typeof null 是 object
在 JavaScript 中,对象都是使用二进制存储的,如果二进制前三位都是 0 的话,则表示 object 类型,而 null 的二进制全是 0,所以是 object。
二、客户端Web应用的两个生命周期阶段是什么?
客户端Web应用的生命周期的两个阶段是页面构建和事件处理。在页面构建阶段,页面的用户界面是处理HTML代码和执行主线JavaScript代码。HTML节点处理完成之后,页面进入事件处理阶段,执行各种事件的处理。
三、相比将事件处理器赋值给某个特定元素的属性上,使用addEventListener方法来注册事件处理器的优势是什么?
将事件处理程序分配给特定元素的属性,我们只能注册一个事件处理器;使用addEventListener,我们能够注册必要的多个事件处理器。
四、JavaScript引擎在同一时间能处理多少个事件?
基于单线程的执行模型,一次只能处理一个事件
五、事件队列中的事件是以什么顺序处理的?
处理顺序是按照事件生成的顺序,事件处理阶段大量依赖事件队列,所有的事件都以其出现的顺序存储在事件队列中。事件循环会检查事件队列的队头,如果检测到一个事件,那么相应的事件处理器就会被调用。
六、JS单线程好处
避免频繁的上下文切换
七、事件循环
1、宏任务和微任务
宏任务:一般是由浏览器发起的,比如script、setTimeout、setInterval、I/O等
微任务:一般是由JS自身创建的,比如Promise、process.nextTick等
2、事件循环
由于js是单线程的,浏览器为了能处理一些异步任务,引入了事件循环机制。每一次循环的任务如下:
- 执行栈选择最先进入队列的宏任务(一般都是script),执行其同步代码直至结束;
- 检查是否存在微任务,有则会执行至微任务队列为空;
- 如果宿主为浏览器,可能会渲染页面;
- 开始下一轮循环,执行宏任务中的异步代码(setTimeout等回调)。
3、async、await和promise的执行顺序
- Promise优先于setTimeout宏任务。所以,setTimeout回调会在最后执行。
- Promise一旦被定义,就会立即执行。
- Promise的reject和resolve是异步执行的回调。所以,resolve()会被放到回调队列中,在主函数执行完和setTimeout前调用。
- await执行完后,会让出线程。async标记的函数会返回一个Promise对象。
async function async1(){
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2(){
console.log('async2')
}
console.log('script start')
setTimeout(function(){
console.log('setTimeout')
},0)
async1();
new Promise(function(resolve){
console.log('promise1')
resolve();
}).then(function(){
console.log('promise2')
})
console.log('script end')
输出结果:script start -> async1 start -> async2 -> promise1 -> script end ->async1 end -> promise2 -> setTimeout
八、箭头函数和普通函数的区别
- 箭头函数的 this 对象,是定义时所在的对象,而普通函数的 this 对象,是调用时所在的对象。
- 箭头函数内部没有 arguments 变量,而普通函数可以使用 arguments 变量获取参数。
- 箭头函数无法使用 new 关键字来实例化,是因为它没有原型(prototype)。
九、this指向问题
1、普通函数调用,此时this指向window
function fn(){
console.log(this) // window
}
fn() // window.fn(),此处默认省略window
2、构造函数调用,此时this指向实例对象
function Person(age, name){
this.age = age
this.name = name
console.log(this) // 此时this分别指向Person的实例对象p1 p2
}
var p1 = new Person( 18, 'zs')
var p2 = new Person( 18, 'ww')
3、对象方法调用,此时this指向该方法所属的对象
var obj = {
fn: function(){
console.log(this) // obj
}
}
obj.fn()
4、通过事件绑定的方法,此时this指向绑定事件的对象
<body>
<button id='btn'>hh</button>
<script>
var oBtn = document.getElementById('btn')
oBtn.onclick = function(){
console.log(this) // btn
}
</script>
</body>
5、定时器函数,此时this指向window
setInterval(function(){
console.log(this) // window
},1000)
十、apply,call和bind
- 三者都可以改变函数的this对象指向。
- 三者第一个参数都是this要指向的对象,如果如果没有这个参数或参数为undefined或null,则默认指向全局window。
- 三者都可以传参,但是apply是数组,而call是参数列表,且apply和call是一次性传入参数,而bind可以分为多次传入。
- bind 是返回绑定this之后的函数,便于稍后调用;apply 、call 则是立即执行 。
十一、垃圾回收机制
1、内存泄漏
不再用到的内存,没有及时释放,就叫做内存泄漏。
2、垃圾回收
解决内存的泄露,垃圾回收机制会定期(周期性)找出那些不再用到的内存(变量),然后释放其内存。现在各大浏览器通常采用的垃圾回收机制有两种方法:标记清除,引用计数。
1)标记清除
当变量进入执行环境(函数中声明变量)的时候,垃圾回收器将其标记为“进入环境”,当变量离开环境的时候(函数执行结束)将其标记为“离开环境”,在离开环境之后还有的变量则是需要被删除的变量。垃圾收集器给内存中的所有变量都加上标记,然后去掉环境中的变量以及被环境中的变量引用的变量的标记。在此之后再被加上的标记的变量即为需要回收的变量,因为环境中的变量已经无法访问到这些变量。
2)引用计数
当声明了一个变量并且将一个引用类型赋值给该变量的时候这个值的引用次数就为 1,如果同一个值又被赋给另一个变量,那么引用数加 1;如果该变量的值被其他的值覆盖了,则引用次数减 1。当这个值的引用次数变为 0 的时候,说明没有变量在使用,这个值没法被访问了,回收空间,垃圾回收器会在运行的时候清理掉引用次数为 0 的值占用的内存。
如果一个值不再需要了,引用数却不为0,垃圾回收机制无法释放这块内存,从而导致内存泄漏。此时,要手动解除引用。
十二、作用域和作用域链
1、作用域
作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。ES5 只有全局作用域没和函数作用域,ES6 增加块级作用域。
暂时性死区:在代码块内,使用 let 和 const 命令声明变量之前,该变量都是不可用的,语法上被称为暂时性死区。
函数的作用域在函数定义的时候就决定了。
2、作用域链
当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。
十三、闭包
闭包是指有权访问另外一个函数作用域中的变量的函数,可以理解为能够读取其他函数内部变量的函数。
闭包的用途:
1、模仿块级作用域
所谓块级作用域就是指在循环中定义的变量,一旦循环结束,变量也随之销毁,它的作用范围只在这一小块。
2、储存变量
闭包的另一个特点是可以保存外部函数的变量,内部函数保留了对外部函数的活动变量的引用,所以变量不会被释放。
3、封装私有变量
我们可以把函数当作一个范围,函数内部的变量就是私有变量,在外部无法引用,但是我们可以通过闭包的特点来访问私有变量。
十四、原型和原型链
1、原型
每个对象都有一个隐式原型 __proto__ 属性,如果对象是通过 new 构造函数创建的,那么它的 __proto__ 指向的是构造函数的 prototype,例如:
const obj = {}; // 隐式创建的对象
console.log(obj.__proto__ === Object.prototype); // true
function People(name) {
this.name = name;
}
const p = new People("1024nav");
console.log(p.__proto__ === People.prototype); // true
还有一个特殊的 Function 对象,Function 的 __proto__ 指向的是自身的 prototype。
console.log(Function.__proto__ === Function.prototype); // true
2、原型链
一个对象的隐式原型 __proto__ 指向构造函数的 prototype,然而构造函数 prototype 的 __proto__ 也是指向构造函数的构造函数的 prototype,这样就形成了一条链,一直到顶部为 null。
function People(name) {
this.name = name;
}
const p = new People("1024nav");
console.log(p.__proto__ === People.prototype); // true
console.log(p.__proto__.__proto__ === Object.prototype); // true
console.log(p.__proto__.__proto__.__proto__ === null); // true
十五、new运算符的实现机制
- 首先创建了一个新的空对象;
- 设置原型,将对象的原型设置为函数的prototype对象;
- 让函数的this指向这个对象,执行构造函数的代码(为这个新对象添加属性);
- 判断函数的返回值类型,如果是值类型,返回创建的对象;如果是引用类型,就返回这个引用类型的对象。
十六、继承
// 定义一个动物类作为父类
function Animal (name) {
// 属性
this.name = name || 'Animal';
// 实例方法
this.sleep = function(){
console.log(this.name + '正在睡觉!');
}
}
// 原型方法
Animal.prototype.eat = function(food) {
console.log(this.name + '正在吃:' + food);
};
1、原型链继承:将父类的实例作为子类的原型
function Cat () {
}
Cat.prototype = new Animal();
Cat.prototype.name = 'cat';
2、构造继承:直接利用call或者apply方法将父类构造函数的this绑定为子类构造函数的this
function Cat (name) {
Animal.call(this);
this.name = name || 'Tom';
}
3、实例继承:为父类实例添加新特性,作为子类实例返回
function Cat (name) {
var instance = new Animal();
instance.name = name || 'Tom';
return instance;
}
4、拷贝继承
function Cat (name) {
var animal = new Animal();
for(var p in animal){
Cat.prototype[p] = animal[p];
}
this.name = name || 'Tom';
}
5、ES6中Class继承
class Animal{
constructor(){
this.kind = 'animal';
}
sleep(){
console.log(this.name + '正在睡觉’);
}
}
class Cat extends Animal{
constructor(name){
super();
this.name = name;
}
}
十七、常见数组api
- 不改变原数组
- 将数组转化为字符串(String)
- 数组拼接(concat)
- 数组选取,获取子数组(slice 含头不含尾)
- 返回某个指定的字符串值在字符串中首次出现的位置(indexOf)
- 改变原数组
- 从数组中添加/删除项目,然后返回被删除的项目(splice(i,n)从第i个开始的n个元素)
- 反转数组元素(reverse)
- 对数组的元素进行排序(sort)
- 向数组的末尾添加一个或多个元素,并返回新的长度(push)
- 删除并返回数组的最后一个元素(pop)
- 用于把数组的第一个元素从其中删除,并返回第一个元素的值(shift)
- 向数组的开头添加一个或更多元素,并返回新的长度(unshift)
- 迭代方法(以下方法都不会修改数组中包含的值)
- every():对数组中的每一项运行给定的函数,如果该函数对每一项都返回true,则返回true。
- filter():对数组中的每一项运行给定的函数,返回该函数会返回true的项组成的数组。
- forEach():对数组中的每一项运行给定的函数。这个方法没有返回值。
- map():对数组中的每一项运行给定的函数,返回每次函数调用的结果组成的数组。
- some():对数组中的每一项运行给定的函数,如果该函数对任一项返回true,则返回true。
- 归并方法
- reduce():从数组中的第一项开始,逐个遍历到最后。
- reduceRight():从数组的最后一项开始,向前遍历到第一项。
十八、数组转字符串
- toString() :将数组转换成一个字符串
- toLocalString() :将数组转换成本地约定的字符串
- join() :将数组元素连接起来以构建一个字符串
十九、正则表达式
字符 | 描述 |
---|---|
\ | 将下一个字符标记为一个特殊字符、或一个原义字符、或一个向后引用、或一个八进制转义符。 |
^ | 匹配输入字符串的开始位置。 |
$ | 匹配输入字符串的结束位置。 |
* | 匹配前面的子表达式零次或多次。 |
+ | 匹配前面的子表达式一次或多次。 |
? | 匹配前面的子表达式零次或一次。。 |
{n} | n是一个非负整数。匹配确定的n次。 |
{n,} | n是一个非负整数。至少匹配n次。 |
{n,m} | m和n均为非负整数,其中n<=m。最少匹配n次且最多匹配m次。 |
? | 当该字符紧跟在任何一个其他限制符(*,+,?,{n},{n,},{n,m})后面时,匹配模式是非贪婪的。非贪婪模式尽可能少的匹配所搜索的字符串,而默认的贪婪模式则尽可能多的匹配所搜索的字符串。 |
. | 匹配除“\ n ”之外的任何单个字符。要匹配包括“\ n ”在内的任何字符,请使用像“(.|\n) ”的模式。 |
x|y | 匹配x或y。 |
[xyz] | 字符集合。匹配所包含的任意一个字符。 |
[^xzy] | 负值字符集合。匹配未包含的任意字符。 |
[a-z] | 字符范围。匹配指定范围内的任意字符。 |
[^a-z] | 负值字符范围。匹配任何不在指定范围内的任意字符。 |
\b | 匹配一个单词边界,也就是指单词和空格间的位置。 |
\B | 匹配非单词边界。 |
\d | 匹配一个数字字符。等价于[0-9]。 |
\D | 匹配一个非数字字符。等价于[^0-9]。 |
\f | 匹配一个换页符。 |
\n | 匹配一个换行符。 |
\r | 匹配一个回车符。 |
\s | 匹配任何空白字符,包括空格、制表符、换页符等等。 |
\S | 匹配任何非空白字符。 |
\t | 匹配一个制表符。 |
\w | 匹配包括下划线的任何单词字符。等价于“[A-Za-z0-9_] ”。 |
\W | 匹配任何非单词字符。等价于“[^A-Za-z0-9_] ”。 |
常用正则表达式
用户名 | /^[a-z0-9_-]{3,16}$/ |
---|---|
密码 | /^[a-z0-9_-]{6,18}$/ |
十六进制值 | /^#?([a-f0-9]{6}|[a-f0-9]{3})$/ |
电子邮箱 | /^([a-z0-9_\.-]+)@([\da-z\.-]+)\.([a-z\.]{2,6})$/ /^[a-z\d]+(\.[a-z\d]+)*@([\da-z](-[\da-z])?)+(\.{1,2}[a-z]+)+$/ |
URL | /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/ |
IP 地址 | /((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)/ /^(?:(?: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]?)$/ |
HTML 标签 | /^<([a-z]+)([^<]+)*(?:>(.*)<\/\1>|\s+\/>)$/ |
删除代码\\注释 | (?<!http:|\S)//.*$ |
Unicode编码中的汉字范围 | /^[\u2E80-\u9FFF]+$/ |
二十、深拷贝和浅拷贝
1、深拷贝:修改新变量的值不会影响原有变量的值。默认情况下基本数据类型(number,string,null,undefined,boolean)都是深拷贝。
2、浅拷贝:修改新变量的值会影响原有的变量的值。默认情况下引用类型(object)都是浅拷贝。
二十一、DOM和BOM
1、DOM(Document Object Model) 对象的全称是文档对象模型,定义了访问HTML文档对象的一套属性、方法和事件。
2、BOM (Browser Object Model)对象的全称是浏览器对象模型,提供了能与浏览器窗口进行交互的对象。一般有:
- window 对象,例如 window.open
- document 对象,例如 document.getElementById('app')
- location 对象,例如 location.reload()
- screen 对象,例如 screen.width
- history 对象,例如 history.back()
二十二、事件流
事件流描述的是从页面中接收事件的顺序。
1、冒泡型事件流:事件的传播是从最特定的事件目标到最不特定的事件目标。即从DOM树的叶子到根。
2、捕获型事件流:事件的传播是从最不特定的事件目标到最特定的事件目标。即从DOM树的根到叶子。
二十三、DOM事件流三个阶段
捕获阶段 -> 处于目标阶段 -> 冒泡阶段
二十四、addEventListener的第三个参数有什么用
addEventListener 的函数签名如下,第三个参数表示是否使事件捕获,默认是 false,即使用事件冒泡。
element.addEventListener(event, function, useCapture);
二十五、事件委托
事件委托也叫事件代理,是利用事件冒泡原理,把子元素的事件交给父元素来代理。
二十六、js会阻塞页面渲染吗
JS引擎和渲染引擎是互斥的,因此JS执行的时候渲染引擎就会挂起,所以会阻塞页面的渲染。
二十七、defer和async的区别
1、defer:不会停止(阻塞)dom 树构建,立即异步加载。加载好后,如果 dom 树还没构建好,则先等 dom 树解析好再执行;如果 dom 树已经准备好,则立即执行。
2、async:不会停止(阻塞)dom 树构建,立即异步加载,加载好后立即执行
二十八、设计模式
1、单例模式
实现方式:使用一个变量存储类实例对象(值初始为 null/undefined
)。进行类实例化时,判断类实例对象是否存在,存在则返回该实例,不存在则创建类实例后返回。多次调用类生成实例方法,返回同一个实例对象。单例模式的特点,意图解决:维护一个全局实例对象。
应用场景:
- 引用第三方库(多次引用只会使用一个库引用,如 jQuery)
- 弹窗(登录框,信息提升框)
- 购物车 (一个用户只有一个购物车)
- 全局态管理 store (Vuex / Redux)
2、发布订阅模式
核心:订阅者、发布者、信号中心(事件中心)。
我们假定:存在一个“信号中心”,某个任务执行完成,就向信号中心“发布”一个信号,其他任务可以想信号中心“订阅”这个信号,从而知道什么时候可以开始执行。
应用场景:
- 子组件和父组件的通信方式
- 兄弟组件通信
3、观察者模式
核心:观察者(Watcher)、目标(Dependency依赖)。
观察者: 每个观察者必须有一个update()方法,当事件发生时,执行观察者的update()。观察者可以理解为发布/订阅模式的订阅者。
目标:可以理解为发布/订阅模式的发布者。
应用场景:
- 在vue中,数据实时更新视图
二十九、节流和防抖
1、节流:事件触发后,规定时间内,事件处理函数不能再次被调用。也就是说在规定的时间内,函数只能被调用一次,且是最先被触发调用的那次。
2、防抖:多次触发事件,事件处理函数只能执行一次,并且是在触发操作结束时执行。也就是说,当一个事件被触发准备执行事件函数前,会等待一定的时间。