# 数据类型√
# JS数据类型√
- 原始类型8种:number、string、boolean、null、undefined、
- ES6添加的:symbol、bigInt
- 引用类型:Object、Array、函数
栈存放指针、堆存放实体
# 判断数据类型的办法√
- typeof 判断基本数据类型,和函数,不能准确判断null
- instanceof 判断对象类型,依据原型链判断
- Object.prototype.toString.call() 返回调用者的具体类型,所有基础类型都能判断,对象数组也可以
# 0.1+0.2为什么不等于0.3
进制转换:IEEE754,最大存储53为有效数字,大于的会被截掉导致精度丢失
解决办法:转为大数或整数运算
# new操作符具体做了什么 √
function myNew(context) {
// 创建一个新的对象
const obj = new Object();
// 设置空对象的__proto__为构造函数的prototype
obj.__proto__ = context.prototype;
// 构造函数的this指向这个对象,执行构造函数的代码
const res = context.apply(obj, [...arguments].slice(1));
// 判断函数的返回值类型,如果是引用类型,就返回这个引用类型的对象
return typeof res === "object" ? res : obj;
}
# dom.onclick和dom.addEventListener区别?√
dom.onclick:元素被点击是发生
- 可以在内联标签上添加事件,只能有一个,后来的会覆盖前者
- 潜在安全问题XSS跨站脚本攻击
- 内联不能分离文档结构和逻辑
- 只能处理冒泡阶段
dom.addEventListener:用于向元素添加事件,三个参数分别是事件、回调函数、是否在捕获和冒泡阶段执行
- 可以添加多个事件,按注册顺序执行
- 分离文档结构和逻辑
- 可以处理冒泡和捕获阶段,true表示在捕获阶段处理,false表示在冒泡阶段处理
# 作用域和闭包√
# 作用域
作用域:代码的当前上下文,控制着变量和函数的可见性和生命周期。最大的作用是隔离变量,不同作用域下同名变量不会冲突。
作用域链:当前作用域没有查到值,会向上级作用域查询,直到全局作用域。
作用域分类:
全局作用域:代码在程序的任何地方都能被访问,例如window对象。生命周期和页面生命周期等同。
模块作用域:AMD、CommonJS、ES6等等。模块有自己独立的作用域
函数作用域:闭包在该作用域产生。
块级作用域:JS的变量提升存在变量覆盖、变量污染的情况,ES6引入了块级作用域let、const
# 执行上下文(重点)
执行一段代码时的准备工作,成为执行上下文,用执行上下文栈管理执行上下文
每个执行上下文,有三个重要属性:
- 变量对象VO,是执行上下文相关的数据作用域,存储了上下文中定义的变量和函数声明。
- 作用域链[[scope]],活动对象AO保存在作用域链的最前端
- this
函数执行时上下文的初始化:(注意:父函数执行时,子函数才会被创建)
- 复制函数[[scope]]属性创建作用域链
- 用arguments创建活动对象AO
- 初始化AO,加入形参、函数声明、变量声明
- 将AO压入函数作用域链的顶端
- 子函数被创建,保存作用域链到函数的内部属性[[scope]]
# 全局上下文
全局上下文的变量对象就是全局对象
# 函数上下文
在函数上下文中,用活动对象AO表示变量对象。当进入一个执行上下文中,这个上下文的变量对象才会被激活,所以叫活动对象。通过函数的arguments属性初始化。
进入执行上下文时,会先处理函数声明,在处理变量声明,变量对象包括:
- 函数的所有形参
- 函数声明
- 变量声明
在代码执行阶段,会顺序执行代码,根据代码,修改变量对象的值
function foo() {
console.log(a);
a = 1;
}
foo(); // Uncaught ReferenceError: a is not defined
function bar() {
a = 1; //a会被绑定在全局作用域
console.log(a);
}
bar(); // 1
console.log(foo); // 函数,因为会优先处理函数声明
function foo(){
console.log("foo");
}
var foo = 1;
# 函数创建(复制)
函数创建时,就会把所有的父变量对象保存在内部属性[[scope]]中,此时不包含自身的变量对象
# 函数激活(添加)
Scope = [AO].concat([[Scope]]);
函数激活时,进入函数上下文,创建VO/AO之后,就会将活动对象添加到作用域链的前端
# 作用域链
查找变量时,会先从当前上下文的变量对象中查找,没有找到,会从父级执行上下文查找,直到查找全局上下文。
# this
Reference,是一种规范中的类型,用于描述语言底层行为逻辑
var foo = 1;
// 对应的Reference是:
var fooReference = {
base: EnvironmentRecord,// 属性所在的对象
name: 'foo',//属性的名称
strict: false
};
获取Reference组成部分的方法:
- GetBase:返回reference的base value
- IsPropertyReference:如果base value是一个对象,就返回true
- GetValue:返回具体的值
函数调用:
- 计算MemberExpreesion,结果赋给ref
- 判断ref是不是Reference类型,如果是:
- 如果IsPropertyReference(ref) 是 true, 那么 this 的值为 GetBase(ref)
- 或者base value 是Environment Record,那么那么this的值为 ImplicitThisValue(ref)
- 否则,this为undefined
MemberExpreesion是什么?简单理解就是表达式中()左边的部分
var value = 1;
var foo = {
value: 2,
bar: function () {
return this.value;
}
}
//示例1 foo.bar this->foo
console.log(foo.bar());
//示例2 (foo.bar)和foo.bar一样 this->foo
console.log((foo.bar)());
//示例3 GetValue this->undefined,会被隐式转换为全局对象
console.log((foo.bar = foo.bar)());
//示例4 GetValue this->undefined
console.log((false || foo.bar)());
//示例5 GetValue this->undefined
console.log((foo.bar, foo.bar)());
# 闭包
# 理解闭包
表象:函数A包含了函数B,函数B使用了函数A的变量,那么函数B就被称为闭包,闭包就是能够 读取函数A内部变量的函数。
实质:在某个内部函数的执行上下文创建时,会将父级函数的活动对象加到内部函数的[[scope]]中,形成作用域链,所以即使父级函数的执行上下文销毁,但是活动对象还是存储在内存中,可以被内部函数访问到,所以实现了闭包。
闭包的特征:
- 函数中存在函数
- 内部函数可以访问外部函数的额作用域
- 外部函数的参数和变量不会被GC,始终留在内存中
- 使用闭包会消耗内存
# 应用场景
- for循环闭包变量i
// demo1 输出 3 3 3 var定义的是全局变量
for(var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
// demo2 输出 0 1 2 let具有块级作用域
for(let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
// demo3 输出 0 1 2 闭包
for(let i = 0; i < 3; i++) {
(function(i){
setTimeout(function() {
console.log(i);
}, 1000);
})(i)
}
- 模拟私有方法
/* 模拟私有方法 */
// 模拟对象的get与set方法
var Counter = (function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
})();
- setTimeout中使用
// setTimeout(fn, number): fn 是不能带参数的。使用闭包绑定一个上下文可以在闭包中获取这个上下文的数据。
function func(param){ return function(){ alert(param) }}
const f1 = func(1);
setTimeout(f1,1000);
- 实现继承
// 以下两种方式都可以实现继承,但是闭包方式每次构造器都会被调用且重新赋值一次所以,所以实现继承原型优于闭包
// 闭包
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
this.getName = function() {
return this.name;
};
this.getMessage = function() {
return this.message;
};
}
# 预解析和变量提升 √
预解析:JS引擎将JS所有var声明和function都提升到作用域的最前面
变量提升:将所有的变量声明提升到作用域的最前面
# call、apply、bind √
call和apply是使用后马上执行,bind是返回一个新的函数
call与apply的区别在于传参,call需要把参数列举出了
call实现:
Function.prototype.myCall = function (context) {
// 判断调用对象
if (typeof this !== "function") {
throw new Error("Type error");
}
// 首先获取参数
let args = [...arguments].slice(1);
let result = null;
// 判断 context 是否传入,如果没有传就设置为 window
context = context || window;
// 将被调用的方法设置为 context 的属性
// this 即为我们要调用的方法
context.fn = this;
// 执行要被调用的方法
result = context.fn(...args);
// 删除手动增加的属性方法
delete context.fn;
// 将执行结果返回
return result;
};
apply的实现:
Function.prototype.myApply = function (context) {
if (typeof this !== "function") {
throw new Error("Type error");
}
let result = null;
context = context || window;
// 与上面代码相比,我们使用 Symbol 来保证属性唯一
// 也就是保证不会重写用户自己原来定义在 context 中的同名属性
const fnSymbol = Symbol();
context[fnSymbol] = this;
// 执行要被调用的方法
if (arguments[1]) {
result = context[fnSymbol](...arguments[1]);
} else {
result = context[fnSymbol]();
}
delete context[fnSymbol];
return result;
};
bind实现:
Function.prototype.myBind = function (context) {
// 判断调用对象是否为函数
if (typeof this !== "function") {
throw new Error("Type error");
}
// 获取参数
const args = [...arguments].slice(1),
const fn = this;
return function Fn() {
return fn.apply(
this instanceof Fn ? this : context,
// 当前的这个 arguments 是指 Fn 的参数
args.concat(...arguments)
);
};
};
# ...args剩余参数和arguments对象的区别
- ...args只包含没有形参的实参,而arguments包含了传给函数的所有实参
- ...args是真正的Array实例,arguments不是真正的数组
- arguments还有附加属性callee
# 遍历方法
# 遍历数组
- for循环
- forEach() 不能执行break和return
- map()
- for...of 可迭代的对象都可遍历
# 遍历对象
- for...in 遍历一个对象自由的、继承的、可枚举的、非Symbol的属性名
- Object.keys() 返回一个可枚举属性名组成的数组
- Object.values() 可枚举属性值
- Object.getOwnPropertyNames() 返回指定对象的所有自身属性名
# 遍历字符串
- for...of
- for循环
# JS的NAN属性
NaN属性代表非数字值的特殊值,不可配置,不可写
NaN!==NaN
isNaN()判断是否为NaN
# null和undefined
undefined == null //true
undefined === null //false
let a
typeof a //undefined
let b= null
typeof b //object
null表示空对象指针,JS的最初版本使用32位系统,为了性能考虑使用低位存储变量类型信息,000开头代表对象,而null表示为全零
# ===和==
==对比时,若类型不相同,会先转换为相同类型,然后再比较值
- 一个是null、一个是undefined、相等
- 一个是字符串、一个是数值、字符串转换为数值
- true/false转换为1/0
- 一个是对象、一个是数值和字符串
- 先尝试valueOf()
- toString()
===不会转换类型
# 函数的length√
- 形参的个数
function fn1 () {}
function fn2 (name) {}
function fn3 (name, age) {}
console.log(fn1.length) // 0
console.log(fn2.length) // 1
console.log(fn3.length) // 2
- 第一个具有默认值之前的参数个数
function fn1 (name) {}
function fn2 (name = '林三心') {}
function fn3 (name, age = 22) {}
function fn4 (name, aaa, age = 22, gender) {}
function fn5(name = '林三心', age, gender, aaa) {}
console.log(fn1.length) // 1
console.log(fn2.length) // 0
console.log(fn3.length) // 1
console.log(fn4.length) // 2
console.log(fn5.length) // 0
# DOM事件流√
事件三要素:事件源、绑定事件、添加事件处理程序
事件流:
- 捕获阶段:DOM最顶层window开始,逐级向下寻找目标元素传播的过程,遇见绑定的捕获事件会向下传递
- 目标阶段:到达目标阶段出发绑定事件
- 冒泡阶段:事件开始由具体元素接受,逐级向上传播到DOM顶层window的过程
有了事件流,就可以控制事件在事件流的那一个环节执行
# 中文的长度
js是unicode编码,所有的字符都是一个
前后端不一致的情况,可以用正则替换后再获取长度
# 简述cookie/session记录登录状态机制原理
第一次访问服务器,服务器会开辟一块空间来存放用户信息。
每一个登录之后的用户信息,都会以key-value格式记录在session中
同事服务器会把sessionId存在cookie中返回给客户端
客户端把sessionId保存在本地cookie中对应的网站记录下
下次访问携带这个sessionId
# 简述cookies、session、sessionStorage、localStorage区别
cookie
- 把数据存在用户的浏览器中
- 刷新页面不会丢失数据
- 只能存储字符串
session
- 位于web服务器,主要负责访问者与网站的交互,关闭网站时会话就已经结束
sessionStorage
- 生命周期为关闭浏览器
- 同一个窗口数据可共享
- 2.5m
localStorage
- 生命周期为永久
- 多个窗口共享
- 5m
cookie与sessionStorage、localStorage区别:
- cookie始终在同源的http请求中携带,另外两个不会自动发送到服务器
- cookie不能超过4k,另外两个可到5M以上
- 数据有效期不同:sessionStorage仅在浏览器窗口关闭之前有效,localStorage始终有效,窗口或浏览器关闭也一直保存;cookie在过期时间之前有效
- 作用域不同:sessionStorage在同一个的浏览器窗口共享,localStorage和cookie在所有同源窗口共享
# for-in与for-of区别 √
- for-in:用于遍历对象可枚举属性,包括对象的原型链中的属性,遍历数组的索引
- for-of:用于遍历可迭代对象,不会遍历原型,遍历数组的值,对象部署了Symbol.iterator属性就可以遍历
# for...of与Iterator
遍历器Iterator提供一种接口,为不同的数据结构提供统一的访问机制,即for...of循环
手写next方法
function makeIterator(array){
var nextIndex = 0
return {
next: function(){
return nextIndex < array.length ?
{value:array[nextIndex++],done:false}:
{done:true}
}
}
}
var iter = makeIterator([1,2,3])
console.log(iter.next())
console.log(iter.next())
console.log(iter.next())
console.log(iter.next())
一个数据结构只要具有Symbol.iterator属性,就认为是可以遍历的。Symbol.iterator属性本身是一个函数,执行这个函数,返回一个遍历器。
对象没有默认部署Iterator接口
调用了Iterator接口的场合:
- for...of
- Array.from()
- Map(),Set()
- Promise.all()
- Promise.race()
# 常见的事件
- 点击事件:onclick、ondblclick
- 焦点事件:onblur失去焦点、onfocus获得焦点
- 加载事件:onload
- 鼠标事件:onmosedown、onmouseup
- 键盘事件:onkeydown
- 选择和改变:onchange
- 表单事件:onsubmit
# JS严格模式
"use strict" 减少优于隐式转换、全局变量污染带来的安全问题
- 变量必须声明再使用
- 预编译时this为undefined,
- 不支持arguments、caller、callee、with
- 函数内部不能通过arguments修改参数值
- 不允许对变量或函数使用delete
- 不允许变量重名
- 不允许使用8进制
- 不可对对象的只读属性赋值
- 全局this不再指向window对象
# ~~字符√
~是按位取反,~~会保持原值,并转化为int类型
正值相当于Math.floor(x)
# Map和Object区别
- 内存占用:同样的内存,map比object多存储50%的键值对
- 插入性能:有大量插入操作时map性能更好
- 查找速度:大数据量时差异较小,某些情况Object更好
- 删除性能:选择map
# 位运算符√
位与&、位或|、位异或^、位非~
左移<<、右移>>、无符号右移>>>、无符号左移<<<
# delete删除性能
- 对象不存在也返回true,不能通过结果来判断
- 对象的属性存在并且不能删除时返回false
- 全局作用域或者var、function声明的无法用delete删除
# 浏览器缓存各个文件的具体缓存配置
Content-Length加Last-modified的哈希
# val、let、const
var:
- 声明提升
- 同一个作用域可以多次声明,最开始的会被覆盖
- 没有块级作用域
- 会绑定在顶层window对象上,其他两个不会
const:
- 不可重新赋值
- 引用类型例外
- 必须同时初始化和赋值
- 栈保存引用、堆保存值
let:
- 不会声明提升
- 可更改
- 不可重复声明
# 将值设置不可修改
Object.freeze:
- 不能添加删除属性
- 不能修改已有属性
- 对象原型也不能修改
Object.seal:
- 不能添加删除
- 可以修改已有属性
Object. preventExtensions:
- 不能添加新属性
# 箭头函数和普通函数√
箭头函数:
- 没有prototype属性,不能作为构造函数使用
- 不能绑定arguments,会继承上一级作用域的arguments
- this指向定义时父级作用域的this
主要解决问题:
- 简化函数写法,多用于回调函数
- 解决this指向问题,函数不再是一等公民
不能通过call、apply、bind绑定
# symbol类型√
定义一个唯一的对象属性名,不可改变。当使用一个别人提供的对象时,想要添加一些属性,可能会重复,那么就可以使用Symbol
- 作为属性名,解决全局变量名冲突的问题
- 不能用new构建 var s1 = Symbol("dish")
- 作为属性key,只能通过括号获取,不能用点获取
- 不能被for-in迭代
Symbol.description可以读取描述信息
symbol.for()返回的可以是相等的
Symbol.for("bar") === Symbol.for("bar")
// true
Symbol("bar") === Symbol("bar")
// false
# 异步√
调用栈call stack,也就是执行上下文栈
异步任务的执行,首先会进入调用栈,解释器会将其响应的回调任务放在任务队列中,紧接着调用栈会删除这个任务。当主线程清空后,解释器会读取任务队列,并依次将已完成的异步任务加入调用栈执行。
# event loop、宏任务和微任务
宏任务(浏览器规定的):整体代码块,setTimeout、setInterval、MessageChannel、IO操作、事件队列、setImmediate(Node环境)
微任务(ES6语法规定的):Promise.[then|catch|finally]、MutationObserver(浏览器环境)、process.nextTick(Node环境)
区别:
- 宏任务在DOM渲染后触发
- 微任务在DOM渲染前触发
事件循环的具体流程:
- 从宏任务队列中,按照入队顺序,找到第一个执行的宏任务,放入调用栈,开始执行
- 执行完该宏任务下所有的同步任务后,该宏任务被推出宏任务队列,然后微任务开始按照入队顺序,依次执行,直到微任务队列清空,微任务中产生微任务会立即添加在队尾
- 微任务队列清空后,一个事件循环结束
- 接着从宏任务队列,找到下一个宏任务,开始第二轮循环。
# async/await
async将一个同步函数变为异步函数,返回值变为promise
await可以放在任何异步的、基于promise的函数之前,执行过程中会暂停在该代码上,直到promise完成。
async函数的函数体可以被看做是0个或者多个await表达式分开的,从函数的第一行开始到第一个await都是同步运行的。因此,如果async函数有一个await表达式,那么async函数就会异步执行
# promise对象
三个状态:
- Pending :初始状态,表示异步操作还在进行中
- Fulfilled:表示异步操作成功,此时会执行.then()方法的回调函数
- Rejected:异步操作失败,执行.catch()方法的回调函数
创造一个Promise实例
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
Promise构造函数接受一个函数作为参数,该函数的两个参数分别为resolve和reject,它们是两个函数,由JS提供,不用自己部署。
resolve函数的作用是,将Promise对象的状态从pending变为resolved,在异步函数操作成功时调用,并将异步操作的结果作为参数传递出去;reject函数的作用是,将Promise对象的状态从pending转为rejected,在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
Promise实例生成后,可以用then方法分别指定resolved状态和rejected状态的回调函数
promise.then(function(value) {
// success
}, function(error) {
// failure
});
如果调用resolve
函数和reject
函数时带有参数,那么它们的参数会被传递给回调函数
# Promise.then()
作用是为Promise实例添加状态改变后的回调函数。返回一个新的Promise
# Promise.catch()
相当于.then(null,rejection)或者.then(undefined,rejection)的别名。
这三种写法是等价的:
const promise = new Promise(function(resolve, reject) {
throw new Error('test');
});
promise.catch(function(error) {
console.log(error);
});
// Error: test
// 写法一
const promise = new Promise(function(resolve, reject) {
try {
throw new Error('test');
} catch(e) {
reject(e);
}
});
promise.catch(function(error) {
console.log(error);
});
// 写法二
const promise = new Promise(function(resolve, reject) {
reject(new Error('test'));
});
promise.catch(function(error) {
console.log(error);
});
Promise对象的错误具有冒泡性质,会一直向后传递,直到被捕获为止。所以只需要在链式的尾部添加一个catch即可,通常不要在then里面处理reject
# Promise.finally() ES2018
不管promise对象最后状态如何,都会执行的操作,因为都用的resolve
Promise.prototype.finally = function (callback) {
let P = this.constructor;
return this.then(
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(() => { throw reason })
);
};
# Promise.all()
用于将多个Promise实例,包装为一个新的Promise实例
const p = Promise.all([p1, p2, p3]);
p的状态由p1、p2、p3决定:
- 只有p1、p2、p3的状态都变为fulfilled,p->fulfilled,返回数组
- 只有有一个被rejected,p->rejected,返回第一个被reject的实例的返回值
- 如果作为参数的p。定义了自己的catch方法,一旦被rejected,并不会触发Promise.all()d的catch方法
# Promise.reslove()
将现有的对象转为Promise对象
参数的情况:
- 参数是一个Promise实例,返回这个实例
- 参数是thenable对象,转为Promise对象后,立即执行它的then方法
- 参数不是thenable对象或者根本不是对象,返回一个新的对象,状态为resolved
- 不带任何参数,返回一个resolved状态的Promise对象
# Promise.reject()
返回一个新的 Promise 实例,该实例的状态为rejected
# arguments对象√
- callee用于匿名函数递归
- 本身不是数组,但有length属性,可以索引元素
- 检测参数个数
- 模拟函数重载
# ArrayBuffer
操作二进制数据的一个接口
- ArrayBuffer代表原始二进制数据
- TypedArray 读写简单的二进制数据
- DataView读写复杂二进制数据
# 浏览器工作原理和V8引擎
# 浏览器内核
实质上是浏览器的排版引擎、页面渲染引擎、样板引擎
- webkit :苹果基于KHTML开发的,用于safari
- Blink:webkit一个分支,用于Google、edge等
# 浏览器渲染过程√
# 生成渲染树
- 解析HTML文件,构建DOM树
- CSS解析,构建CSSOM树
- 合并形成一个渲染树 Render tree
- Layout回流,确定节点在屏幕中的位置和大小
- Painting:根据渲染树以及回流得到的几何信息,得到节点的绝对像素
- Display:像素发送给GPU,展示在页面上
# 回流
构造渲染树时,需要将可见的DOM及其样式结合起来,还需要计算在视口内的确切位置和大小。计算的阶段就是回流。
# 重绘
将渲染树的每个节点转换为屏幕的实际像素
# v8引擎
- JS代码、词法语法分析、抽象语法树、解释为字节码、运行结果
- 上下文栈、预解析、变量提升、作用域、暂时性死区
- 基本类型存放在栈中分配
- 复杂类型存放在堆中,引用的指针在栈中
# 垃圾回收机制√
两种策略:
- 标记清除:标记阶段为所有活动对象做标记,清除阶段把没有标记的销毁。
- 缺点:内存碎片化、分配速度慢
- 采用标记整理算法
- 引用计数:如果没有引用指向该对象,对象将被垃圾回收机制回收
- 缺点:需要一个计数器,所占内存空间大,解决不了循环引用的问题
v8的垃圾回收机制基于标记清除
# 常见手写
# 防抖
事件被触发时,设置一个定时器。如果在定时器内事件再次触发,那么久清除之前的定时器,重新设置一个新的定时器。只有当定时器时间到达之后,才会真正的触发事件。可以保证,事件被频繁触发的时候,只有最后一次会被执行。
# 节流
对于高频率触发的事件,限制触发的时间间隔,以减少对系统资源的消耗