JS 基础
20200513
1、借鉴 Vue 源码的检测方法
该方法返回描述某个对象数据类型的字符串,如自定义的对象没有被覆盖,则会返回“[object type]”,其中,type则是实际的对象类型,故使用slice(8,-1)
let _toString = Object.prototype.toString;
function toRawType (value) {
// 获取 从第九个到倒数第二个 字符
// 比如 [object String] 获取 String
return _toString.call(value).slice(8, -1)
}
// console.log(toRawType('abc'))
2、JS 中的真假值
在隐式转换中,假值会被转换成 false ,真值会被转换成 true
JS 中除了假值外都是真值,其中假值有7个:
undefined、null、’‘、0、-0、false、NaN
3、引用类型
基础类型:String、Number、Boolean、undefined、null、Symbol(较新标准)、bigInt(新标准)
NaN 也属于number,并且 NaN 不等于其本身
1.引用类型:除了基础类型外,其他都是引用类型;(常见引用类型:Object,Array,Function,Date,RegExp,单体内置对象(Math,window(客户端才有的全局对象),global(服务端才有的全局对象)))
2.引用类型在创建的时候会分配 两个空间:
一块在'堆'上,储存 引用类型 本身的数据(当然数据量会比较大)
一块在'栈'上,储存 ’堆‘ 上数据的引用(储存堆上的内存地址,也就是指针)
3.引用类型 是可变的,即 let a = {}; a.x = 1
4.function()参数是值传递,要注意不能修改引用
4、如何检测引用类型
1.通过 Object.prototype.toString.call检测[[calss]] (检测为[Object type] Object.prototype.toString.call().slice(8,-1))
2.通过 instanceof 判断 引用类型
3.通过constructor判断引用类型(constructor是可写的,慎用)
扩展:typeof 用于检测基本数据类型,不建议用于检测引用类型,因为 typeof检测[],{},null 都返回Object,检测function 返回 Function
instanceof 用于检测 引用类型,不建议检测继承类型,因为检测基础类型都返回 false , 因为基础类型没有 __proto__
instanceof 的内部机制是通过判断对象的原型链中是不是能找到对应的 prototype
function _instanceof(obj, target) { // 实现instanceof
// 获得对象原型
obj = obj.__proto__
// 判断对象的类型是否等于对象的原型
while(true) {
// 如果 obj === null 说明原型链遍历完毕
if (obj === null) {
return false
}
// 如果 obj.__proto__ = target.prototype, 说明对象是该类型的实例
if (obj.__proto__ === target.prototype) {
return true
}
// 原型链上查找
obj = obj.__proto__
}
}
// console.log('_instanceof: ',_instanceof([],Object))
5、函数的定义
1.每个函数实际上都是一个 Function 对象,即 (function() {}).constructor === Function
2.函数是头等公民/一等公民
- 函数可以像其他任何对象一样具有属性和方法
- 可以赋值给变量(函数表达式)
- 可以作为参数传递给函数(高阶函数)
- 可以作为另一个函数的返回值(闭包)
3.定义函数的四种方式:
- new Function(str) (new Function声明的对象是在函数创建时解析的,故比较低效)
- 函数表达式 var fn = function() {}
- 函数声明 function fn() {}
- 箭头函数 var fn = () => {}
6、闭包
MDN 定义:函数与对其状态,即词法环境的引用共同构成闭包(closure),即闭包可以让你从内部函数访问外部函数作用域
小红书定义:闭包就是指有权访问另一个函数作用域中的变量的函数
产生闭包的原因:
- 根据垃圾回收机制的 可达算法,不可达就会被回收,不可达简单的说就是堆内存中没有在栈内存中存放引用(即没有指针指向堆)就可视为不可达
使用闭包的缺点:
1、性能考量: 闭包在处理速度和内存消耗方面对脚本性能具有负面影响(多执行一个函数,多了一个内存指向)
2、可能内存溢出。(比如:在闭包中的 addEventListener 没有被 removeEventListener)
function saySomething(){ // 闭包实例 1
var name = 'mokou'
return function () { // saySomething方法的返回值的引用存在了say变量中,所有可达,故引用不会被销毁,从而产生了闭包
// console.log(name)
}
}
var say = saySomething()
say()
闭包使用场景 1:请求出错的提示框(多个请求同时出错一般都只有一个提示框)
使用单例模式设计
const Singleton = (function(){
let _instance // 优点:_instance 是私有的,外部不能更改(保证安全无污染/可信)
return function(obj) {
return _instance || (obj === _instance)
}
})()
let a = new Singleton({x:1})
let b = new Singleton({y:2})
// console.log('闭包场景1:', a === b)
闭包使用场景 2:解决 var 在 for + setTimeout 混合场景中的 bug
产生原因:因为 var 是函数作用域(原因1),而setTimeout是异步执行(原因2),所以:当console.log()时执行的时候 i 已经等于 6 了(bug产生)
for (var i = 0; i <= 5; i++) {
setTimeout(function() {
// console.log('闭包场景 2:',i) // 打印输出 6 6 6 6 6 6
}, i 300)
}
// 不使用 let const 的解决方法就是使用闭包
for (var i = 0; i <= 5; i++) {
(function(j) {
setTimeout(function() {
// console.log('闭包场景 2解决方案:',j) // 打印输出 0 1 2 3 4 5
}, j 300)
})(i)
}
7、函数表达式和函数声明的区别
1.主要区别在于:
- 函数声明被提升到了函数定义(可以在函数声明之前使用)
- 函数表达式要根据定义的方式进行判断
+ 通过 var 定义:有变量声明提升
+ 通过 let 和 const 定义:没有变量提升
2.变量提升:JavaScript 中,函数和变量(通过 var 定义)的声明都将被提升到函数的最顶部
3.函数定义和变量同名时,在生产执行上下文时会有两个阶段
- 创建的阶段(具体步骤是创建 VO, variable object 活动对象),JS 解释器会找到需要提升的函数和变量,并且给它们提前在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只是声明并且赋值为 undefined
- 代码执行阶段:我们可以直接提前使用
4.在提升的过程中:函数定义优先于变量提升,变量在执行阶段才会被真正赋值
// console.log(typeof a7 === 'function') // true 优先执行函数提升
var a7 = 1
function a7() {}
// console.log(a7 === 1)
8、箭头函数
1.箭头函数中的 this,就是定义时所在的对象
2.一旦绑定了上下文就不可改变(call,apply,bind 都不能改变箭头函数内部 this 的指向)
3.由于 this 问题,箭头函数不能用作构造函数,不能使用 new 命令
4.箭头函数没有 arguments,需要手动使用 ...args 参数代替
5.箭头函数不能用作 Generator 函数
let obj = {
x () {
let y = () => {
// console.log('8箭头函数:',this === obj)
}
y() // true
// call、apply、bind 都不能改变箭头函数内部 this 的指向
y.call(window) // true
y.apply(window) // true
y.bind(window)() // true
// 同时,被bind绑定过的方法,也是不可变的,(不会再次被 bind、call、apply改变this的指向)
}
}
// obj.x()()
9、其他函数中 this 指向问题
1.以函数形式调用(this 指向 window)
2.以方法形式调用(this 指向调用函数的对象)
3.以 call, apply, bind的形式调用(更改指向,箭头函数除外)
4.以构造函数调用(this 指向实例)
扩展1:call 和 apply 的区别
- 入参不同(apply的入参为数组,call的入参为字符串)
- 性能差异(call 比 apply 快很多)
+ 原因:.apply 在运行前要对作为参数的数组进行一系列检验和深拷贝,.call 则没有这些步骤
扩展2:call 和 apply 都是直接返回函数执行后的结果,而bind是返回一个函数,需要手动执行会后才会返回结果
扩展3:bind,apply,call都是改变this的指向
- call,apply不传参数一样
- 有参数call一次传入(call(element,foo1,foo2,foo3)),有参数apply传入一个数组参数(apply(element,[foo1,foo2,foo3]))
- call,bind有参数传入一样
- bind后面要加()执行,bind其实创建了一个函数,调用就需要加()执行
function fn () { // 以函数形式调用(this 指向 window)
console.log('9其他函数中this的指向问题1:',this, 'fn')
function subFn () {
console.log(this, 'subFn')
}
subFn() // window
}
fn(); // window
var x = 'abc'
var obj = { // 以方法形式调用(this 指向调用函数的对象)
x: 123,
fn: function () {
console.log('9其他函数中this的指向问题1:',this.x);
}
}
obj.fn() // 123
var fn = obj.fn
fn() // abc
10、for in 、for of 、 forEach、map 各自的区别
1.for in 遍历的是对象可枚举属性
2.for of 遍历的是对象的迭代器属性
3.forEach 只能遍历数组,且不能中断(break等无效)
4.map 返回一个新数组
11、手写一个防抖函数 和 节流函数
1.防抖函数使用场景
- 输入框校验
2.节流函数使用场景
- 延迟防抖函数:onScroll 时触发的事件
- 立即执行防抖函数:按钮的点击事件(某种情况下 once 函数更合适)
function debounce(fn, wait) { // 防抖函数
let timer = null;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, wait);
}
}
function throttle(fn, wait = 300) { // 节流函数
let flag = true;
return (...args) => {
if (!flag) return;
flag = false;
setTimeout(() => {
fn.apply(this, args);
flag = true;
}, wait);
}
}
12、Class 和 function 有什么不同
1.ES6 中的 Class 可以被看作一个语法糖,它的绝大部分功能,ES5都可以做到,新的 Class 写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已
2.Class 和 function 有什么不同
- 类没有变量提升
- 类的所有方法都不可以枚举
- 类的所有方法没有原型对象 prototype
- 类不能直接使用,必须要 new 调用
- 类内部启用严格模式
13、new 的过程
1.创建一个空对象
2.新对象的 __proto__ 指向构造函数的 prototype
3.绑定 this,指向构造方法
4.返回新对象
function myNew() {
var obj = new Object() // 创建一个新对象
var Constructor = [].shift.call(arguments) // Constructor 指任意构造函数, [].shift.call(arguments)删除并拿到arguments的第一项,让类数组调用数组的方法
obj.__proto__ = Constructor.prototype // 新对象的 __proto__ 指向构造函数的 prototype
var result = Constructor.apply(obj,arguments) // 绑定 this,指向构造方法
return typeof result === 'object' ? result : obj // 返回新对象
}
14、字符串 string 常用 API
1.charAt() 返回在指定位置的字符
2.charCodeAt() 返回在指定的位置的字符的 Unicode 编码
3.fromCharCode() 从字符编码创建一个字符串
4.concat() 连接字符串
5.indexOf() 检索字符串
6.lastIndexOf() 从后向前搜索字符串
7.match() 找到一个或多个正则表达式匹配
8.replace() 替换与正则表达式匹配的子串
9.search() 检索与正则表达式相匹配的值(大小写敏感),未找到输出-1
10.slice() 提取字符串的片断,并在新的字符串中返回被提取的部分
11.split() 把字符串分割为字符串数组
15、数组常用 API
1.添加
- push() 在数组末尾添加元素
- unshift() 在数组头部添加元素
- concat() 合并两个数组
2.删除
- pop() 删除并返回数组的最后一个元素
- shift() 删除并返回数组的第一个元素
3.子数组
- splice()
+ 删除任意数量的项
- 要删除的起始下标
- 要删除的项数
+ 在指定的位置插入指定的项
- 起始下标
- 0(不删除任何项)
- 要插入的项
+ 替换任意数量的项
- 起始下标
- 要删除的项
- 要插入的项
- slice()
+ 功能
- 从已有数组中选取部分元素构成新数组
+ 参数
- 返回项的起始位置
- 返回项的结束位置
- 参数特性
+ 如果是负数,则用数组长度加上改值确定位置
+ 起始位置为数组的实际下标
+ 结束位置的实际下标为结束数值减1
4.数组排序
- reverse() 颠倒数组中元素的顺序
- sort()
+ 功能
- 对字符数组或者数字数字进行排序
+ 特性
- 默认按字符串进行比较
- 按数值大小进行比较需要函数支持(升序)
function compare(value1,value2){
if (value1 < value2) {
return -1
} else if (value1 > value2) {
return 1
} else {
return 0
}
}
5.数组转换
- toString() 将数组转换为字符串,并返回
- toLocaleString() 转换为本地格式字符串,并返回
- join() 用指定分隔符分割数组并转换为字符串
6.位置方法
- indexOf() 从数组的开始位置进行查找
- lastIndexOf() 从数组的结束位置进行查找
- indexOf 和 lastIndexOf 参数:第一个参数为要查找的项,第二个参数表示查找起点的位置索引
7.迭代方法
- every() 如果该函数对每一项都返回true,则返回true
- filter() 返回为true的所有元素成员
- forEach() 无返回值
- map() 返回每次函数调用的结果数组
- some() 有任意一项返回true,则返回true
- 参数
+ 接收参数
- 要在每一项上运行的函数
- 运行该函数的作用域对象
+ 传入参数
- 数组项的值 item
- 该项在数组中的位置 index
- 数组对象本身 array
8.缩小方法
- reduce() 从数组的起始位置开始遍历
- reduceRight() 从数组的末尾开始遍历
- 参数
+ 接收参数
- 每一项上调用的函数
- 作为缩小基础的初始值
+ 传入参数
- 前一个值 prev
- 当前值 cur
- 项的索引 index
- 数组对象 array
扩展:
1.修改原数组的API有:
splice/reverse/fill/copyWithin/sort/push/pop/unshift/shift
2.不修改原数组的API有:
slice/map/forEach/every/filter/reduce/entries/find
16、从输入URL到页面响应都发生了些什么
1.浏览器的地址栏输入URL并按回车
2.浏览器查找当前URL是否存在缓存,并比较缓存是否过期
3.DNS解析URL对应的IP
4.根据IP建立TCP连接(三次握手)
5.HTTP发起请求
6.服务器处理请求,浏览器接收HTTP响应
7.渲染页面,构建DOM树
8.关闭TCP连接(四次挥手)
17、TCP的三次握手和四次挥手
三次握手
1.第一次握手:客户端发送一个 SYN(synchronize,请求同步) 码给服务器,要求建立数据连接
2.第二次握手:服务器的 SYN 码和自己处理的 SYN(标志);叫 SYN + ACK(确认包,Acknowledgement,确认同步);发送给客服端,建立连接
3.第三次握手:客户端再次发送 ACK 向服务器,服务器验证 ACK 没有问题,则建立起连接
四次挥手
1.第一次挥手:客服端发送 FIN(结束)报文,通知服务器数据已经传输完毕
2.第二次挥手:服务器接收到之后,通知客户端我收到了 SYN ,发送 ACK(确认)给客户端,数据还没有传输完成
3.第三次挥手:服务器已经传输完毕,再次发送 FIN 通知客户端,数据已经传输完毕
4.第四次挥手:客户端再次发送 ACK,进入 TIME_WAIT 状态,之后服务器和客户端关闭连接
18、为什么建立连接是三次握手,断开连接是四次挥手?
1.建立连接的时候,服务器在 LISTEN 状态下,收到建立连接请求的 SYN 报文之后,把 ACK 和 SYN 放在一个报文里面发送给客户端。
2.而关闭连接时,服务器收到对方的 FIN 报文时,仅仅表示对方不再发送数据但是还能接收数据,而自己也未必将全部数据都发送给了对方,所以己方可以立即关闭,也可以发送一些数据给对方后,再发送 FIN 报文给对方来表示同意现在关闭连接,因此,己方 ACK 和 FIN 一般都会分开发送,从而导致多了一次。
20200916
19、类数组和数组的区别是什么?
1.拥有length属性,其它属性(索引)为非负整数(对象中的索引会被当做字符串来处理);
2.不具有数组所具有的方法;
3.类数组是一个普通对象,而真实的数组是Array类型。
4.常见的类数组有: 函数的参数 arugments, DOM 对象列表(比如通过document.querySelectorAll 得到的列表), jQuery 对象 (比如 $("div")).
5.类数组转数组方法:
1)Array.prototype.slice.call(arrayLike, start);
2)[...arrayLike];
3)Array.from(arrayLike);
6.任何定义了遍历器(Iterator)接口的对象,都可以用扩展运算符转为真正的数组。
7.Array.from方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象。
20、 == 和 === 有什么区别?
=== 不需要进行类型转换,只有类型相同并且值相等时,才返回 true.
== 如果两者类型不同,首先需要进行类型转换。具体流程如下:
首先判断两者类型是否相同,如果相等,判断值是否相等.
如果类型不同,进行类型转换
判断比较的是否是 null 或者是 undefined, 如果是, 返回 true .
判断两者类型是否为 string 和 number, 如果是, 将字符串转换成 number
判断其中一方是否为 boolean, 如果是, 将 boolean 转为 number 再进行判断
判断其中一方是否为 object 且另一方为 string、number 或者 symbol , 如果是, 将 object 转为原始类型再进行判断
let person1 = {
age: 25
}
let person2 = person1;
person2.gae = 20;
console.log(person1 === person2); //true,注意复杂数据类型,比较的是引用地址复制
代码思考: [] == ![]
我们来分析一下: [] == ![] 是true还是false?
首先,我们需要知道 ! 优先级是高于 == (更多运算符优先级可查看: 运算符优先级)
![] 引用类型转换成布尔值都是true,因此![]的是false
根据上面的比较步骤中的第五条,其中一方是 boolean,将 boolean 转为 number 再进行判断,false转换成 number,对应的值是 0.
根据上面比较步骤中的第六条,有一方是 number,那么将object也转换成Number,空数组转换成数字,对应的值是0.(空数组转换成数字,对应的值是0,如果数组中只有一个数字,那么转成number就是这个数字,其它情况,均为NaN)
0 == 0; 为true
21、ES6中的class和ES5的类有什么区别?
1.ES6 class 内部所有定义的方法都是不可枚举的;
2.ES6 class 必须使用 new 调用;
3.ES6 class 不存在变量提升;
4.ES6 class 默认即是严格模式;
5.ES6 class 子类必须在父类的构造函数中调用super(),这样才有this对象;ES5中类继承的关系是相反的,先有子类的this,然后用父类的方法应用在this上。
22、数组的哪些API会改变原数组?
1.修改原数组的API有:
splice/reverse/fill/copyWithin/sort/push/pop/unshift/shift
2.不修改原数组的API有:
slice/map/forEach/every/filter/reduce/entries/find
注:
23、let、const 以及 var 的区别是什么?
1.let 和 const 定义的变量不会出现变量提升,而 var 定义的变量会提升。
2.let 和 const 是JS中的块级作用域
3.let 和 const 不允许重复声明(会抛出错误)
4.let 和 const 定义的变量在定义语句之前,如果使用会抛出错误(形成了暂时性死区),而 var 不会。
5.const 声明一个只读的常量。一旦声明,常量的值就不能改变(如果声明是一个对象,那么不能改变的是对象的引用地址)
24、 在JS中什么是变量提升?什么是暂时性死区?
1.变量提升就是变量在声明之前就可以使用,值为undefined。
2.在代码块内,使用 let/const 命令声明变量之前,该变量都是不可用的(会抛出错误)。这在语法上,称为“暂时性死区”。暂时性死区也意味着 typeof 不再是一个百分百安全的操作。
typeof x; // ReferenceError(暂时性死区,抛错)
let x;
typeof y; // 值是undefined,不会报错
暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量
25、 ES6新的特性有哪些?
1.新增了块级作用域(let,const)
2.提供了定义类的语法糖(class)
3.新增了一种基本数据类型(Symbol)
4.新增了变量的解构赋值
5.函数参数允许设置默认值,引入了rest参数,新增了箭头函数
6.数组新增了一些API,如 isArray / from / of 方法;数组实例新增了 entries(),keys() 和 values() 等方法
7.对象和数组新增了扩展运算符
8.ES6 新增了模块化(import/export)
9.ES6 新增了 Set 和 Map 数据结构
10.ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例
11.ES6 新增了生成器(Generator)和遍历器(Iterator)
26、promise 有几种状态, Promise 有什么优缺点 ?
1.promise有三种状态: fulfilled, rejected, pending.
2.Promise 的优点:
1.一旦状态改变,就不会再变,任何时候都可以得到这个结果
2.可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数
3.Promise 的缺点:
1.无法取消 Promise
2.当处于pending状态时,无法得知目前进展到哪一个阶段
27、执行上下文?
代码运行时,产生一个对应的执行环境,这个叫做执行上下文。
通常执行上下文,有三个环境:
1.全局环境:代码首先进入的环境
2.函数环境:函数被调用时执行的环境
3.eval函数
执行上下文,可分为三个阶段,分别为创建,执行,销毁阶段。我们简单的分析一下,各个阶段分别处理了什么。
1.创建阶段:
(1).生成变量对象
(2).建立作用域链
(3).确定 this 指向
2.执行阶段:
(1).变量赋值
(2).函数引用
(3).执行其他代码
3.销毁阶段:
执行完毕出栈,等待回收被销毁