JS
JavaScript
基础
-
JS的数据类型
JS一共有6种基本数据类型分别是undefined, null, boolean, number, string, symbol, 引用数据类型object, array, function
symbol作用: 定义一个独一无二的值
基本类型是保存在栈内存中的简单数据段,它们的值都有固定的大小,保存在栈空间,通过按值访问
引用类型是保存在堆内存中的对象,值大小不固定,栈内存中存放的该对象的访问地址指向堆内存中的对象,JavaScript不允许直接访问堆内存中的位置,因此操作对象时,实际操作对象的引用
-
string是个基本数据类型,为什么可以使用String的一些方法
除去Object, Array等引用类型,JavaScript还提供了三种特殊的引用类型: String, Number和Boolean,方便我们操作对应的基本类型,所以在调用String方法时,并不是基本数据类型stirng执行了自身方法,而是后台为它创建了一个对应的基本包装类型String,它根据基本类型的值实例化了一个实例,让这个实例去调用指定的方法,最后销毁这个实例
-
var, let, const的区别
var声明的变量没有块级作用域而let声明的变量拥有块级作用域,const声明的是常量,声明之后无法对其进行修改, const保证的实际上并不是变量的值不得改变,而是变量指向的那个内存地址不得改动,对于复合类型的数据如对象,数组,变量指向的内存地址保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的
在代码块内,使用let命令声明变量之前,该变量都是不可用的,在语法上称为暂时性死区
-
==, ===, Object.is的区别
==: 会进行强制的类型转换,在转换类型后,二者相等也会返回True,而===不会进行转换, Object.is也不会进行强制类型转换,但与===不同的是,+0===0, Object.is(+0, -0) false, Nan !== Nan, Object.is(Nan, Nan) false
如果Type(x)和Type(y)相同,返回x===y的结果 如果Type(x)和Type(y)不同 如果x是null,y是undefined,返回true 如果x是undefined,y是null,返回true 如果Type(x)是Number,Type(y)是String,返回 x==ToNumber(y) 的结果 如果Type(x)是String,Type(y)是Number,返回 ToNumber(x)==y 的结果 如果Type(x)是Boolean,返回 ToNumber(x)==y 的结果 如果Type(y)是Boolean,返回 x==ToNumber(y) 的结果 如果Type(x)是String或Number或Symbol中的一种并且Type(y)是Object,返回 x==ToPrimitive(y) 的结果 如果Type(x)是Object并且Type(y)是String或Number或Symbol中的一种,返回 ToPrimitive(x)==y 的结果 其他返回false
-
slice()和splice()的区别
slice: 只能截取数组中的一段,且截取后原数据不会发生变化;
splice: 不仅仅能够截取数组中的一段,还能够在原数组中进行替换
-
push(), pop(), shift()和unshift()
(1). push, pop:类似于栈的行为,后进先出
(2). shift, unshift: 类似于队列的行为, 先进先出
数组方法
(1). 修改器方法 push, pop, unshift, shift, sort, reverse, splice (2). 访问方法 (返回新的数组) concat, slice (3). 迭代方法 forEach, filter, map, reduce
-
map和set
map是一组键值对的结构,具有极快的查找速度
set和map类似,也是一组key的集合,但不储存value,在set中,key不可重复
现在,如果我们在 weakMap 中使用一个对象作为键,并且没有其他对这个对象的引用 —— 该对象将会被从内存(和map)中自动清除。
-
null和undefined,怎么判断是null
null: 表示一个值被定义了,定义为"空值";
undefined: 表示根本不存在定义;
let exp = undefined if (typeof (exp) === 'undefined') console.log('undefined'); exp = null if (!exp) console.log('null');
-
new操作符做了哪些事情
用new操作符调用构造函数实际上会经历以下5个步骤:
(1). 创建一个新的对象;
(2). 将构造函数的prototype关联到实例的__proto__;
(3). 将构造函数的作用域赋给新对象(因此this就指向了这个新对象);
(4). 执行构造函数中的代码(为这个新对象添加属性);
(5). 返回新对象;
function Mynew() { let Constructor = Array.prototype.shift.call(arguments) // 1. 取出构造函数 let obj = {} // 2. 创建一个新的对象 obj.__proto__ = Constructor.prototype // 3. 该对象的原型等于构造函数的prototype let result = Constructor.apply(obj, arguments) // 4. 执行函数中的代码 return typeof reuslt === 'object' ? result : obj // 5. 返回值必须是对象 }
new与直接引用的区别
如果函数返回值为常规意义上的值类型(Number、String、Boolean)时,new函数将会返回一个该函数的实例对象,而如果函数返回一个引用类型(Object、Array、Function),则new函数与直接调用函数产生的结果等同
-
this的指向问题
在普通函数中
(1)如果一个函数中有this,但是它没有被上一级的对象调用,那么this的指向就是window;
(2)如果一个函数中有this,这个函数有被上一级调用,那么this指向的就是上一级的对象;
(3)如果一个函数中有this,这个函数中包含多个对象,尽管这个函数是被最外层的对象所调用,this指向的也只是它上一级的对象
this永远指向的是最后调用它的对象
在构造函数中: new关键字是可以改变this的指向的
当this碰到return时,如果返回值是一个对象,那么this指向的就是那个返回的对象,如果返回值不是一个对象那么this还是指向函数的实例
-
箭头函数与普通函数的区别
(1). 箭头函数不会创建自己的this只会从自己的作用链上一层继承this;
(2). 箭头函数继承而来的this指向永远不变所以.call()/.apply()/.bind()无法改变箭头函数中this的指向;
(3). 箭头函数不能作为构造函数使用; 从关键字new的角度去回答,this的指向会发生改变
(4). 箭头函数没有自己的arguments;
(5). 箭头函数没有原型prototype
-
arguments
在调用函数时,我们所传递的实参都会在arguments中保存,是一个类似于数组的对象,只有数组的length,没有数组方法
-
call, apply和bind的用法
call, apply, bind都是用来改变this的指向
apply接收两个参数,第一个参数是this的指向,第二个参数是函数接收的参数,以数组的形式进行传递
call第一个参数也是用来改变this的指向,后面的参数均是函数接收的参数
apply与call均只改变this的指向一次
bind第一个参数也是改变this的指向,后面与call类似也是传入参数列表,但与call不同的是,bind的可以不需要一次性传入所有参数,并且bind改变this指向后不会立即执行而是返回一个永久改变this指向的函数
三者的区别:
(1). 都可以改变this的指向; (2). 第一个参数都是this要指向的对象; (3). 三个都可以传参,但是方法各不相同,apply是数组,bind和call都是利用参数列表进行传参但是bind可以分多次传参,call只可以传一次; (4). bind是返回绑定this之后的函数,apply和call是立即调用
// step 1: 调用函数的上下文指向obj // step 2: 传参 // step 3: 执行函数并返回结果 let myApply = (context) => { context = context ? Object(context) : window context.fn = this let args = [...arguments][1] if (!args) return context.fn() let res = context.fn(...args) delete context.fn return res } let myCall = (context) => { context = context ? Object(context) : window context.fn = this let args = [...arguments].slice(1) let res = context.fn(...args) delete context.fn return res } let myBind = (context) => { let me = this return function () { return me.call(context, arguments) } }
-
Array.sort()的底层实现
在V8引擎中,sort函数只给出了两种排序insertSort和quickSort,数组长度小于等于22的用insertSort大于22的用quickSort
#insertSort function insertSort(arr) { const length = arr.length for (let i = 1; i < length; i++) { const temp = arr[i] let j for (j = i-1; j >= 0 && temp < arr[j]; j--) { arr[j+1] = arr[j] } arr[j+1] = temp } return arr } # quickSort var sortArray = function(nums) { return partition(nums, 0, nums.length-1) };
const partition = (nums, start, end) => { if (start >= end) return nums const pivot = nums[start] let index = start for (let i = start+1; i <= end; i++) { if (nums[i] <= pivot) { index += 1 const temp = nums[i] nums[i] = nums[index] nums[index] = temp } } const temp = nums[index] nums[index] = nums[start] nums[start] = temp partition(nums, start, index-1) partition(nums, index+1, end) return nums } ```
-
CommonJS和ES6模块的区别
(1). CommonJS输出的是一个值的拷贝,ES6模块输出的是值的引用,换句话说就是CommonJS在模块内部发生的后续变化影响不了外部对这个值的使用;
(2). CommonJS模块是运行时加载,ES6模块是编译时输出接口;
(3). CommonJS顶层this指向当前模块, 而在ES6模块中this指向undefined
-
innerHtml, innerText, outerHtml
innerHtml: 是指从对象的起始位置到终止位置的全部内容包括html标签
innerText: 从起始位置到终止位置的内容,不包括html标签
outerHtml: 除了包含innerHtml全部内容外还包含了对象标签本身
火狐浏览器不支持innerText
-
怎么判断是数组
(1). instanceof; (2). constructor arr.constructor === Array; (3). toString example: Object.prototype.toString.call(arr) === '[object Array]';; (4). Array.isArray(arr)
-
数组拷贝
浅拷贝: 直接将数组引用复制的方法是浅拷贝
深拷贝: 递归复制了所有层级 JSON.parse(JSON.stringify(arr)); slice和concat在多维数组中无效
-
Event Loop
为了解决异步任务,js最大的特点是单线程,非阻塞,当在执行栈中有一个异步任务的时候,js不会一直等待其返回结果,而是将这个事件挂起,继续执行执行栈中接下来的任务,当异步任务返回结果后,js会将结果插入到事件队列中,并不会立即回调,当执行栈中任务执行完成之后,会取出事件队列中第一位的事件,放入执行栈中继续执行,如此反复,就形成了一个循环,这个过程就是Event Loop
浏览器环境下,microtask 的任务队列是每个 macrotask 执行完之后执行。而在 Node.js 中,microtask 会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行 microtask 队列的任务。
主代码执行,将 2 个 定时器 依次放入 I/O任务队列最后进入timer队列,主代码执行完毕,调用栈空闲,开始进行事件循环首先进入 timers 阶段,执行 timer1 的回调函数,打印 timer1,并将 promise1.then 回调放入 microtask 队列,同样的步骤执行 timer2,打印 timer2;至此,timer 阶段执行结束,event loop 进入下一个阶段之前,执行 microtask 队列的所有任务,依次打印 promise1、promise2。
而node11之后,node在setTimeOut执行后会手动清空微任务队列,以保证结果贴近浏览器。
-
浏览器事件机制
浏览器事件机制中事件触发的三个阶段: 事件捕获 - 事件目标函数处理 - 事件冒泡
冒泡: 事件会从最内层元素发生,一直向外传播直到document对象
捕获: 与冒泡相反,事件从最外层开始发生直到最具体的元素
当某个元素触发某个事件(如:click),顶级对象document发出一个事件流,顺着dom的树节点向触发它的目标节点流去,直到达到目标元素,这个层层递进,向下找目标的过程为事件的捕获阶段,此过程与事件相应的函数是不会触发的。
到达目标函数,便会执行绑定在此元素上的,与事件相应的函数,即事件目标处理函数阶段。
最后,从目标元素起,再依次往顶层元素对象传递,途中如果有节点绑定了同名事件,这些事件所对应的函数,在此过程中便称之为事件冒泡。
通常情况下,事件相应的函数是在冒泡阶段执行的。addEventListener的第三个参数默认为false,表示冒泡阶段执行(为true的时候,表示捕获阶段执行)。
-
onclick和addEventListener的区别
on: 不可以多次绑定同一事件
addEventListener: 可以多次绑定同一事件,并且并不会覆盖上一个事件
1.onclick事件在同一时间只能指向唯一对象
2.addEventListener给一个事件注册多个listener
3.addEventListener对任何DOM都是有效的,而onclick仅限于HTML
4.addEventListener可以控制listener的触发阶段,(捕获/冒泡)。对于多个相同的事件处理器,不会重复触发,不需要手动使用removeEventListener清除
-
闭包
闭包就是能够读取其他函数内部变量的函数,本质上闭包就是函数内部与函数外部的一座桥梁
-
Promise.all和Promise.race
Promise实现
function (executor) { // 初始属性 this.PromiseState = 'pending' this.PromiseResult = null // 保存回调函数 this.callback = [] // 保存实例对象的this const self = this function resolve (value) { if (self.PromiseState !== 'Pending') return; // 1. 修改对象的状态 self.PromiseState = 'fulfilled' // 2. 设置对象结果值 self.PromiseResult = value // 调用回调函数 setTimeout( () => { self.callbacks.forEach(ele => element.onResolved(value)) }) } function reject (reason) {
if (self.PromiseState !== 'Pending') return; // 1. 修改对象的状态 self.PromiseState = 'rejected' // 2. 设置对象结果值 self.PromiseResult = reason // 调用回调函数 setTimeout( () => { self.callbacks.forEach(ele => element.onRejected(value)) })
} try { excutor(resolve, reject) } catch (error) { reject(error) } } ``` `Promise.all`可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值。 顾名思义,`Promse.race`就是赛跑的意思,意思就是说,`Promise.race([p1, p2, p3])`里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。 ``` Promise.prototype.all = function(promises) { let results = []; let promiseCount = 0; let promisesLength = promises.length; return new Promise(function(resolve, reject) { for (let val of promises) { Promise.resolve(val).then(function(res) { promiseCount++; // results.push(res); results[i] = res; // 当所有函数都正确执行了,resolve输出所有返回结果。 if (promiseCount === promisesLength) { return resolve(results); } }, function(err) { return reject(err); }); } }); }; ```
-
promise与async/await
Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态
一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了
说到底,Promise 也还是使用回调函数,只不过是把回调封装在了内部,使用上一直通过 then 方法的链式调用,使得多层的回调嵌套看起来变成了同一层的,书写上以及理解上会更直观和简洁一些。
promise可解决回调地狱的问题
(1). promise是ES6的语法,async/await是ES7的语法;
(2). promise的错误可以通过catch来捕捉,而async/await的错误用try-catch来捕捉,因为await只会返回promise成功的值;
async与generator
async相当于自执行的generator函数,相当于自带一个状态机,在await的部分等待即过,返回后自动执行下一步
-
JS为什么要区分Microtask和Marcotask
区分微任务和宏任务是为了将异步队列任务划分优先级,通俗的理解就是为了插队。
一个Event Loop,微任务是在宏任务之后调用,微任务会在下一个Event Loop之前执行调用完,并且其中会将微任务执行当中新注册的微任务一并调用执行完,然后才开始下一次 Event Loop,所以如果有新的宏任务 就需要一直等待,等到上一个Event Loop当中 微任务被清空为止。由此可见,我们可以在下一次 Event Loop 之前进行插队。
如果不区分微任务和宏任务那就无法在下一次Event Loop之前进行插队,其中新注册的任务得等到下一个宏任务完成之后才能进行,这中间可能你需要的状态就无法在下一个宏任务中得到同步。
-
ES6相比于ES5的特性
(1). 关键字let - 块级作用域 const - 常量;
(2). 箭头函数;
(3). 解构赋值;
(4). class 只是一个语法糖,并不能代表javascript是一个面向对象的编程语言;
(5). map, set, WeakMap, WeakSet ;
`WeakMap`, `WeakSet`作为属性键的对象如果没有别的变量引用他们,则会被回收释放掉
(6). 扩展运算符(相当于rest的逆运算);
-
防抖和节流
防抖: 当你频繁触发后,n秒内只执行一次, 如果n秒内有被触发,则重新计时
function debounce(fn) { let timeout = null return function (){ clearTimeout(timeout) timeout = setTimeout(() => { fn.apply(this, arguments) }, 500) } }
应用场景: (1).搜索框搜索输入,只需用户最后一次输入完,再发送请求; (2). 手机号,邮箱验证输入检测; (3). 窗口大小Resize,只需窗口调整完成后,计算窗口大小,防止重复渲染;
节流: 在固定的时间内触发事件,每隔n秒触发一次
function throttle(fn, delay) { let canRun = true // 通过闭包保存一个标记 return function () { if (!canRun) return; // 在函数开头判断标记是否为true,不为true则return canRun = false // 立即设置为false setTimeout(() => { fn.appny(this, arguments) canRun = true; }, delay) } }
应用场景: (1). 滚动加载,加载更多或滚到底部监听; (2). 谷歌搜索框,搜索联想功能; (3). 高频点击提交,表单重复提交;
-
js中0.1+0.2为什么不等于0.3,怎么解决
计算机是用二进制储存数的,而十进制小数转二进制的方法是用2去乘小数部分,这会导致小数不能精确的表达所以当两个小数相加时,很有可能产生误差
解决方法是可以将数组转为整数先进行加法再除以他们放大的倍数或者利用第三方库
-
map, forEach
map和forEach的区别:
forEach()方法不会返回执行结果,也就是说forEach会修改原来的数组 map()会得到一个新的数组并返回,不会修改原来的数组 (声明一个新的变量来储存map的结果而不是去修改原数组) ``` let arr = [ { a: 1, b: 2 }, { a: 3, b: 4 } ] const arr1 = arr.map(item => ( { ...item, b: 5 } ))
const arr2 = arr.map(item => { item.b = 5 return item }) console.log(arr); console.log(arr1); console.log(arr2); ```
-
js获取,更新,添加,删除dom操作
// 获取 document.getElementById() // 获取唯一的节点 document.getElementsByTagName (); document.getElementsByClassName(); // 都是获取一组节点 document.querySelector() // 返回文档中匹配指定CSS选择器的第一个元素 document.querySelectorAll()
// 更新 // 修改节点的文本 innerHTML和innerText // 插入 parentElement.appendChild(newElement) // 插入到最后 parentElement.insertBefore(newElement, referenceElement) // 删除 parentElement.removeChild(deleteElement) ```
-
js垃圾回收机制
(1). 标记清理 在垃圾程序运行的时候,会标记内存中存储的所有变量然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉,在此之后再被加上标记的变量就是待删除的了,原因是任何在上下文中的变量都访问不到它们了。随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存 ; (2). 引用计数 对每个值都记录它被引用的次数,声明变量并给它赋一个引用值时,这个值的引用数为1。如果同一个值又被赋给另一个变量,那么引用数加1,类似地,如果保存对该值引用的变量被其他值给覆盖了,那么引用数减1.当一个值的引用数为0时,就说明没发再访问这个值了,因此可以安全地收回其内存了。垃圾回收程序下次运行的时候就会释放引用数为0的值的内存;
垃圾回收算法: (1). 标记空间中可达的值; (2). 回收不可达的值所占据的内存; (3). 做内存整理;
分代收集:
(1). 浏览器将数据分为两种,一种是临时对象,一种是长久对象; (2). 临时对象: 函数内部声明的变量,块级作用域中的变量; 长久对象: 生命周期很长的对象,比如全局的window,DOM (3). 两种不同的对象对应不同的回收策略,V8把堆分成新生代和老生代两个区域,新生代中存放临时对象,老生代中存放持久对象并且让副垃圾回收器,主垃圾回收器分别负责新生代和老生代的垃圾回收,这样就可以高效的垃圾回收 (4). 主垃圾回收器: 负责老生代的垃圾回收,有两个特点: 对象占用空间大,对象存活时间长。它使用标记清除的算法执行垃圾回收。在遍历的过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据,然后是垃圾清除,直接将标记为垃圾的数据清理掉,这样会产生大量的不连续的内存碎片,需要进行内存整理 (5). 副垃圾回收器: 负责新生代的垃圾回收,通常只支持1-8M的容量,新生代被分为两个区域:一般是对象区域,一半是空闲区域。 新加入的对象都被放入对象区域,等对象区域快满的时候,会执行一次垃圾清理。先给对象区域所有垃圾做标记;标记完成后,存活的对象被复制到空闲区域,并且将他们有序的排列一遍; 这就回到我们前面留下的问题 -- 副垃圾回收器没有碎片整理。因为空闲区域里此时是有序的,没有碎片,也就不需要整理了;复制完成后,对象区域会和空闲区域进行对调。将空闲区域中存活的对象放入对象区域里。这样,就完成了垃圾回收。因为副垃圾回收器操作比较频繁,所以为了执行效率,一般新生区的空间会被设置得比较小。一旦检测到空间装满了,就执行垃圾回收。
-
JS如何解决单线程
浏览器的主要线程包括:UI渲染线程,JS主线程,GUI事件触发线程,http请求线程 JS作为脚本语言,它的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。(这里这些问题我们不做研究)
其实JS为我们提供了一个Worker的类,它的作用就是为了解决这种阻塞的现象。当我们使用这个类的时候,它就会向浏览器申请一个新的线程。这个线程就用来单独执行一个js文件。
当然,在主线程中有一些方法来实现对新线程的控制和数据的接收。
在新线程中使用postMessage()方法可以向主线程中发送一些数据,主线程中使用worker的onmessage事件来接收这些数据,这样就实现了js的多线程执行和多线程之间数据的传递。
-
类数组与数组的转换
类数组是一个普通对象,而真实的数组是Array类型。
常见的类数组有: 函数的参数 arguments, DOM 对象列表(比如通过 document.querySelectorAll 得到的列表), jQuery 对象 (比如 $(“div”)).
1)拥有length属性,其它属性(索引)为非负整数(对象中的索引会被当做字符串来处理);
2)不具有数组所具有的方法;
(1). Array.prototype.slice.call(arraylike, start); (2). [...arraylike]; (3). Array.from(arraylike);
-
作用域和作用域链和执行上下文
每个上下文都有一个关联的变量对象,而在这个上下文当中定义的所有变量和函数都存在于这个对象上,上下文在其中所有的代码都执行完毕后会被销毁,包括定义在其中的所有变量和函数,所以上下文决定了它们可以访问哪些数据,上下文可以通过作用域链访问外部上下文中的一切,但外部上下文无法访问内部上下文中的任何东西(执行上下文栈 后入先出)
上下文中的代码在执行的时候,会创建变量对象的作用域链,这个作用域链决定了各级上下文中的代码在访问变量和函数的顺序
变量提升,var的变量提升和函数的变量提升
-
BOM属性和方法
window 浏览器窗口
属性: name:指浏览器窗口的名字或框架的名字。这个名字是给a标记的target属性来用的。设置窗口的名字:window.name = “newWin” 获取窗口的名字:document.write(name); top:代表最顶层窗口。如:window.top parent:代表父级窗口,主要用于框架。 self:代表当前窗口,主要用于框架中。 clientHeight/clientWidth: 代表页面视口的高度和宽度 方法: alert():弹出一个警告对话框。 prompt([text],[defaulttext]):弹出一个输入对话框。 confirm(text):弹出一个确认对话框。如果单击“确定按钮”返回true,如果单击“取* * 消”返回false。 text:要显示的纯文本 close():关闭窗口 print():打印窗口 open([url],[name],[options]):打开一个新窗口 延时器:window.setTimeout(“code”,1000);// code一般是一个函数,但是放在双引号下,1000ms 返回值是延时器的id,给clearTimeout使用 定时器:window.setInterval(“code”,1000);// code一般是一个函数,但是放在”“下,1000ms 返回值是定时器的id,给clearInterval使用
location
属性: href:获取地址栏中完整的地址。可以实现JS的网页跳转。location.href = “http://www.sina.com.cn”; host:主机名 hostname:主机名 pathname:文件路径及文件名 search:查询字符串。 protocol:协议,如:http://、ftp:// hash:锚点名称。如:#top reload([true]):刷新网页。true参数表示强制刷新
history
属性: length 历史记录的个数 方法: go(n):同时可以实现“前进”和“后退。” history.go(0) 刷新网页 history.go(-1) 后退 history.go(1) 前进一步 history.go(3) 前进三步 forward():相当于浏览器的“前进”按钮 back():相当于浏览器的“后退”按钮
-
内存管理
优化内存的最佳手段就是保证内存在执行代码时只保存必要的数据,如果数据不再必要,那么把它设置为null,从而释放其引用
通过const和let声明提升性能(块作用域)
内存泄漏
大部分原因是由不合理的引用导致的 (1). 函数作用域内意外声明全局变量 (2). 不清理定时器 (3). 闭包
-
JS为什么是单线程?
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
-
事件委托
事件委托其实就是利用JS事件冒泡机制把原本需要绑定在子元素的响应事件(click、keydown……)委托给父元素,让父元素担当事件监听的职务。事件代理的原理是DOM元素的事件冒泡。
优点: 大量减少内存占用,减少事件注册。新增元素实现动态绑定事件
<ul id="color-list"> <li>red</li> <li>yellow</li> <li>blue</li> <li>green</li> <li>black</li> <li>white</li> </ul> <script> (function () { var color_list = document.getElementByid('color-list'); color_list.addEventListener('click', showColor, true); function showColor(e) { var x = e.target; if (x.nodeName.toLowerCase() === 'li') { alert(x.innerHTML); } } })(); </script>
-
为什么for...of...不能遍历对象
因为能够被for...of正常遍历的,都需要实现一个遍历器Iterator。而数组、字符串、Set、Map结构,早就内置好了Iterator(迭代器),它们的原型中都有一个Symbol.iterator方法,而Object对象并没有实现这个接口,使得它无法被for...of遍历
代码
-
代码输出
let arr=[1,2,3] let brr=arr.push(4) let crr=arr.splice(1,1,) console.log(arr) // [1, 2, 3] console.log(brr) // 4 console.log(crr) // [2] function a() {} a.key = "123" 会报错吗?为什么? 不会,因为a是引用数据类型可以自定义属性和方法
-
this指向问题
// shoppe var a = 10 var obj = { a: 20, say: () => { console.log(this.a) } } obj.say() // undefined 箭头函数的this继承自它父级的this var anotherObj={a:30} obj.say.apply(anotherObj) // undefined 箭头函数的this不能被bind, apply, call改变 function Person(name) { this.name = name; } Person.prototype.print = function() { return this.name; }; Person('abc'); const a = new Person('abc').print.call({}); console.log(a); // undefined const fn = () => { this.x = 'z'; }; const b = {x: 'y'}; fn.call(b); console.log(b); // {x: 'y'}
-
原型链问题
// shoppe function Parent() { this.a = 'Parent' } function Child() { this.a = 'Child' } Function.prototype.print = function() { console.log(this.a) } Parent.print() // undefined Child.print() // undefined var p = new Parent() p.print() // error
-
作用域问题
// shoppe function(){ var x = y = 1; })(); var z; console.log(y); // 1 console.log(z); // undefined console.log(x); // ReferenceError
-
Event loop
// shoppe console.log(1); setTimeout(() => { console.log(2); Promise.resolve().then(() => { console.log(3) }); }); new Promise((resolve, reject) => { console.log(4) resolve(5) }).then((data) => { console.log(data); }) setTimeout(() => { console.log(6); }) console.log(7); // 1, 4, 7, 5, 2, 3, 6
setTimeout(() => { console.log(1); },0); new Promise(function(resolve){ resolve(); console.log(2); }).then(console.log(3)) console.log(4); function a() { console.log('a'); Promise.resolve().then(() => { console.log('e'); }) } function b() { console.log('b'); } function c() { console.log('c'); } function d() { setTimeout(a, 0) let temp = Promise.resolve().then(b) setTimeout(c, 0) console.log('d'); } d() // d b a c e
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 // script start // async2 // promise1 // script end // async1 end // promise2 // setTimeOut
-
Promise
//打印红绿灯: 打印red,停10s,打印yellow,停2s,打印 green 停 5s。 继续打印red,停10s...,以此类推循环5次。 const red = () => new Promise((resolve, reject) => { console.log('red'); setTimeout(() => { resolve() }, 10 * 1000) }) const yellow = () => new Promise((resolve, reject) => { console.log('yellow'); setTimeout(() => { resolve() }, 2 * 1000) }) const green = () => new Promise((resolve, reject) => { console.log('green'); setTimeout(() => { resolve() }, 5 * 1000) }) const main = (curr, count) => { red().then(() => { return yellow() }) .then(() => { return green() }) .then(() => { curr += 1 if (curr < count) main(curr, count) }) } main(0, 5)
-
继承
当谈到继承时,JavaScript 只有一种结构:对象。每个实例对象( object )都有一个私有属性(称之为 proto )指向它的构造函数的原型对象(prototype )。该原型对象也有一个自己的原型对象( proto ) ,层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。
ES5的继承机制简单来说就是:实质是先创造子类的实例对象,然后再将父类的方法添加到this上面(Parent.apply(this))
ES6的继承实质上是先创建父类的实例对象this(所以必须先调用父类的super()方法),然后再用子类的构造函数修改this
es6的子类必须要调用super来生成this
es5 (1). 通过原型进行继承,在子类构造函数中调用父类的构造函数两者组合使用实现继承效果
function Student(name, age) { this.name = name this.age = age } Student.prototype.study = function () { console.log('study') } function collegeStu (name, age, school) { Student.call(this, name, age) // 若不指定this,直接调用父类的构造函数,this会指向window this.school = school } //collegeStu1._proto_ === collegeStu.prototype 没有study //student1._proto_ === Student.prototype 有study //要想找到study方法,就让这个{collegeStu.prototype}对象成为Student的实例对象
let student1 = new Student('Steven', 10) student1.study() // study // 让子类大学生的显式原型成为父类,学生的实例对象,同时将构造器指向子类构造函数本身 collegeStu.prototype = new Student() collegeStu.prototype.constructor = collegeStu let collegeStu1 = new collegeStu('Allen', 20, 'UNSW') collegeStu1.study() // 继承父类的方法,study ```
es6 1.本质上构造函数只是函数而已,并不是真正的类 2.用class定义一个类,对象中会包含一个constructor方法,相当于一个构造函数,也称为构造器,在其中子类可以用super()函数调用父类的构造方法 3.类中也可以定义一般的方法,相当于构造函数.prototype去定义方法 ``` class Student { constructor(name, age) { this.name = name this.age = age } study () { console.log('study') } } class collegeStu extends Student { constructor(name, age, school) { super(name, age) this.school = school } } let student1 = new Student('小明', 10) student1.study() // study let collegeStu1 = new collegeStu('小华',20,'xxx大学') collegeStu1.study() // 继承父类的方法 学习 ``` js实现extend ``` var obj1 = {'a': 'obj2','b':'2'}; var obj2 = {name: 'obj3'}; function extend() { var length = arguments.length; var target = arguments[0] || {}; if (typeof target!="object" && typeof target != "function") { target = {}; } if (length == 1) { target = this; i--; } for (var i = 1; i < length; i++) { var source = arguments[i]; for (var key in source) { // 使用for in会遍历数组所有的可枚举属性,包括原型。 if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; } console.log(extend(obj1,obj2)); ``` ``` function inheritPrototype(subType, superType){ //原型式继承:浅拷贝superType.prototype对象作为superType.prototype为新对象的原型 // 内部会自带_proto_指向:prototype.\_\_proto\_\_ = superType.prototype; var prototype = Object.create(superType.prototype); // subType.prototype.\_\_proto\_\_ = superType.prototype; subType.prototype = prototype; // 将子类的原型替换为这个原型 prototype.constructor = subType; // 修正原型的构造函数 } function SuperType(name){ this.name = name; this.colors = ["red", "blue", "green"]; } SuperType.prototype.sayName = function(){ alert(this.name); }; function SubType(name, age){ SuperType.call(this, name); this.age = age; } // 核心:因为是对父类原型的复制,所以不包含父类的构造函数,也就不会调用两次父类的构造函数造成浪费 inheritPrototype(SubType, SuperType); SubType.prototype.sayAge = function(){ alert(this.age); } ```