js 中基础数据类型有哪几种?了解包装对象么?
基础数据类型有6种:string、number、boolean、null、undefined、symbol。
基础数据类型都是值,所以没有方法提供调用,例如:undefined.split('')。
那为什么比如 "abc"split('') 类似这种调用可以被允许?原因是 js 中存在包装对象,会把字符串先包装成对象,然后再调用对象下的一些方法,方法调用完成之后再销毁对象。这样就完成了基础数据类型的函数调用功能。
JavaScript 允许访问字符串,数字,布尔值和 symbol 的方法和属性。为了使它们起作用,创建了提供额外功能的特殊 “对象包装器”,使用后即被销毁。
基本类型和引用类型数据存储方式?(栈内存,堆内存)
在JS中,主要通过栈和堆这两种数据结构存储数据。用户可以直接操作栈,但不允许直接操作堆。
- 栈:比较小,存储变量和基本数据类型
- 堆:比较大,存储引用数据类型
如何判断一个数据是 NaN
NaN 非数字 但是用 typeof 检测是 number 类型。
- 利用
isNaN(NaN) => true - 利用
NaN != NaN - 利用ES6 中提供的 Object.is()方法(判断两个值是否相等)
Object.is(NaN,NaN) === true
Js 中 null 与 undefined 区别
相同点∶用 if 判断时,两者都会被转换成 false
不同点∶Number(null)为0;Number(undefined)为 NaN
Null表示一个值被定义了,但是这个值是空值
Undefined 变量声明但未赋值
Script标签中的async和defer属性有什么区别呢?
-
async:表示脚本会在“后台”下载,并在加载就绪时执行(加载优先顺序)。多个async不能保存执行顺序。- 不会阻塞
DOMContentLoaded。
- 不会阻塞
-
defer:表示脚本会在“后台”下载,然后等 DOM 构建完成后,脚本才会执行。。有多个defer,按出现的顺序执行。- 在
DOMContentLoaded事件之前执行。
- 在
DOMContentLoaded 与 load 的区别
DOMContentLoaded浏览器已完全加载 HTML,并构建了 DOM 树,但像<img>和样式表之类的外部资源可能尚未加载完成。
document.addEventListener("DOMContentLoaded", ready);
-
load浏览器不仅加载完成了 HTML,还加载完成了所有外部资源:图片,样式等。 -
beforeunload用户正在离开:我们可以检查用户是否保存了更改,并询问他是否真的要离开。 -
unload用户几乎已经离开了,但是我们仍然可以启动一些操作(只能执行不涉及延迟或询问用户的简单操作),例如发送统计数据。- 我们可以使用
navigator.sendBeacon来发送网络请求(数据大小限制在 64kb)。它会在后台发送数据,转换到另外一个页面不会有延迟:浏览器离开页面,但仍然在执行sendBeacon。
- 我们可以使用
ES6新特性
const 与 let,结构赋值,for...of,对象字面量,模板字面量,展开运算符,箭头函数,默认函数参数,class类。
var let const
var可重复声明;声明时挂在window对象上;函数作用域;会变量提升;let不允许重复声明;不会通过window访问;块级作用域;不会变量提升;声明前使用会造成“暂时性死区”;const不允许重复声明;一旦声明就不允许更改(注意下对象,内层可以被更改); 不会通过window访问;块级作用域;不会变量提升;
数组遍历查找
-
arr.forEach()对数组中的每一个元素,执行一次提供的函数,返回undefined。不能中断循环。 -
arr.find()对数组中的每一个元素,执行一次提供的函数,查找 第一个 具有特定条件的元素,找到返回该元素(结束查找),否则返回undefined:- 如果
function()返回true,则搜索停止,并返回item。如果没有搜索到,则返回undefined。
- 如果
-
arr.findIndex()与arr.find()基本上是一样的,但它返回找到元素的索引,而不是元素本身。并且在未找到任何内容时返回-1。 -
arr.filter()对数组中的每一个元素,执行一次提供的函数,查找符合条件的所有元素,并作为一个新数组返回。 -
arr.map()对数组中的每一个元素,执行一次提供的函数,返回执行结果组成的新数组。 -
arr.some()测试数组中是否 至少有一个元素 通过了指定函数的测试,结果返回布尔值。 -
arr.every()测试数组中是否 所有元素 都 通过了指定函数的测试,结果返回布尔值。
类数组和数组有什么区别?类数组转数组的方法有什么?
类数组与数组的区别:
-
相同点:都可用下标访问每个元素,都有length属性。
-
不同点:数组对象的类型是Array,类数组对象的类型是object;类数组不具有数组所具有的方法,数组遍历可以用 for in和for循环,类数组只能用for循环遍历。
类数组转数组:
Array.prototype.slice.call(arrayLike, start);Array.from(arrayLike);- 扩展运算符
…
介绍下 Set、Map、WeakSet 和 WeakMap 的区别?
Set:“值的集合”(没有键),它的每一个值只能出现一次。
WeakSet:成员都是对象;成员都是弱引用,可以被垃圾回收机制回收,可以用来保存 DOM 节点,不容易造成内存泄漏;
Map:本质上是键值对的集合,就像一个 Object 一样,但是它们最大的差别是 Map 允许任何类型的键(key),而 Object 的键只能是字符串。
WeakMap:只接受对象最为键名(null 除外),不接受其他类型的值作为键名;键名是弱引用,键值可以是任意的,键名所指向的对象可以被垃圾回收,此时键名是无效的;不能遍历。
在 Map 和 Set 中迭代总是按照值插入的顺序进行的,所以我们不能说这些集合是无序的,但是我们不能对元素进行重新排序,也不能直接按其编号来获取元素。
WeakMap 和 WeakSet 最明显的局限性就是不能迭代,并且无法获取所有当前内容。
new 实现原理
- 创建一个空对象 obj
- 将该对象 obj 的原型链
__proto__指向构造函数的原型 prototype,并且在原型链__proto__上设置 构造函数 constructor 为要实例化的 Fn - 传入参数,并让 构造函数 Fn 改变指向到 obj,并执行
- 最后返回 obj
箭头函数与普通函数的区别
箭头函数是匿名函数,不能作为构造函数,不能使用 new
箭头函数不能绑定 arguments,要用 rest 参数解决
箭头函数没有原型属性
箭头函数不能绑定 this,会捕获其所在的上下文的 this值,作为自己的this值
如何判断 this ?箭头函数的 this 是什么?
可以分成三种场景来描述
1、函数中直接调用,默认情况下 this 指向 window
function fn () {
console.log(this)
}
fn() // this 指向 window
默认情况下 this 指向 window。但在 严格模式 和 <script type="module"> 下,this 为 undefined。
2、在对象里调用,this 指向调用的对象
let obj = {
name: 'una',
fn: function () {
console.log(this)
}
}
obj.fn() // this 指向调用的对象
3、在构造函数及类中,this 指向实例化的对象
function Person () {
this.name = 'una'
}
Person.prototype.fn = function () {
console.log(this)
}
let una = new Person()
una.fn() // this 指向实例化的对象 una
箭头函数没有自己的 this,会继承外层的 this。
function Person () {
this.name = 'una'
}
Person.prototype.fn = () => {
console.log(this)
}
let una = new Person()
una.fn() // this 指向实例化的对象 una
.call() .apply() .bind() 的区别和作用
作用:改变函数执行时的上下文。简而言之就是改变函数运行时的 this 指向(默认指向 window)。
区别:调用方式和参数传递不同
-
apply 和 call
- 第一个参数都是要改变指向的对象
- 第二个参数,apply 是数组,而call 则是 arg1,arg2... 这种形式。
-
bind
- 返回一个新的函数,这个函数不会马上执行,需要手动执行。参数可以放在执行时传递,也可以放在绑定 this 时传入。
function fn(...args){
console.log(this, args)
}
fn(1, 2) // 默认指向 `window`
let obj = { name: 'una' }
fn.call(obj, 1, 2)
fn.apply(obj, [1, 2]) // 传入参数必须是数组
fn.bind(obj)(1, 2) // bind 不是立即执行,需要手动执行。参数可以放在执行时传递,也可以放在绑定 this 时传入。
fn.bind(obj, 1)(2)
扩展: bind 实现
// context 是第一个参数,就是我们要绑定的 this 指向
Function.prototype.myBind = function (context) {
// 判断调用对象是否为函数
if (typeof this !== "function") {
throw new TypeError('It's not function');
}
let args = [...arguments].slice(1); // 获取参数(第一个是要绑定的this指向,要剔除掉)
let fn = this;
// bind 要手动执行,显而易见它应该返回一个函数
return function () {
// bind 参数可以放在执行时传递,也可以放在绑定 this 时传入。所以要把参数进行合并
return fn.apply(context, args.concat(...arguments));
}
}
此时执行 fn.myBind(obj, 1)(2) 结果与之前的一样。
但是如果我们换一种执行方法,通过 new 去执行函数:
let foo = fn.bind(obj)
let res = new foo(1, 2);
可以看到原生的 bind 此时返回的 this 是实例化的 fn 函数。但是我们自己实现的 bind 就不行了:
let foo1 = fn.myBind(obj)
let res1 = new foo(1, 2);
此时返回的 this 是 obj,与原生的效果不符。让我们稍微修改一下:
...
return function Fn() {
// 如果是用 Fn 函数构造的就返回它的实例
return fn.apply(this instanceof Fn ? new fn : context, args.concat(...arguments));
}
此时再执行就没有问题了。
完整版代码:
// context 是第一个参数,就是我们要绑定的 this 指向
Function.prototype.myBind = function (context) {
// 判断调用对象是否为函数
if (typeof this !== "function") {
throw new TypeError('It's not function');
}
let args = [...arguments].slice(1); // 获取参数(第一个是要绑定的this指向,要剔除掉)
let fn = this;
// bind 要手动执行,显而易见它应该返回一个函数
return function () {
// bind 参数可以放在执行时传递,也可以放在绑定 this 时传入。所以要把参数进行合并
// 如果是用 Fn 函数构造的就返回它的实例
return fn.apply(this instanceof Fn ? new fn : context, args.concat(...arguments));
}
}
什么是回调?回调使用中存在什么问题?
答案:回调即是函数指针的调用,即是一个通过函数指针调用的函数。如下代码:
function foo (callback){
callback && callback();
}
foo(() => {
console.log('我是回调函数')
})
使用回调函数有一个很大缺点就是造成回调地狱,回调地狱是为了实现某些逻辑出现函数的层层嵌套,类似如下代码:
move(ele, 300, "1eft", functlon () {
move(ele, 300, "top", functlon () {
move(ele, 0, "1eft", functlon () {
move(ele, 0, "top", functlon () {
console.log("所有运动完毕")
})
})
})
)
回调地狱会造成代码可读性及可维护性变差。同样每个嵌套函数耦合性强,一层变动会引起其他的结果变动。同样回调地狱如果出现错误不好处理错误。
解决回调地狱问题可以通过观察者横式、 promise 、async/await 来处理。
async await
promise
Promise.allSettled 是什么?如何实现?
Promise.allSettled 是 ES2020 新特性,可以执行多个 Promise 对象,获取多个 Promise 对象状态,无论成功或失败。
Promise.all 执行多个 Promise 对象,成功执行则返回对象状态数组。但必须是 resolve 状态,否则会报错。
let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
// resolve(1)
reject('error')
}, 1000)
})
let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(2)
}, 1000)
})
Promise.all([p1, p2]).then(res => {
console.log(res)
})
// [1, 2]
Promise.allSettled([p1, p2]).then(res => {
console.log(res)
})
Promise.all 若将其中一个 Promise 对象状态改为 reject,则会报错。
而 Promise.allSettled 不会报错。
扩展:Promise.allSettled 实现
function MyAllSettled (list) {
let resArr = new Array(list.length) // 收集结果,数组长度为待执行的 Promise 个数
let num = 0 // 执行次数
// Promise.allSettled 返回的结果可以 .then,说明需要返回一个 Promise
return new Promise(resolve => {
list.forEach((item, index) => {
// 每个 Promise 去获得结果
item.then(res => {
// resolve
let obj = {
status: 'fulfilled',
value: res
}
resArr[index] = obj
num++
if(num === list.length){
resolve(resArr)
}
}, err => {
// reject
let obj = {
status: 'rejected',
reason: err
}
resArr[index] = obj
num++
if(num === list.length){
resolve(resArr)
}
})
})
})
}
async await对比promise的优缺点
setTimeout、Promise、Async/Await 的区别
事件循环
问题:什么是同步?什么是异步?
同步和异步是一种消息通知机制。
-
同步阻塞:A 调用 B,B 处理获得结果,才返回给 A。A 在这个过程中,一直等待 B 的处理结果,没有拿到结果之前,需要 A (调用者)一直等待和确认调用结果是否返回,拿到结果,然后继续往下执行。
做一件事,没有拿到结果之前,就一直在这等着,一直等到有结果了,再去做下边的事。
-
异步非阻塞:A 调用 B,无需等待 B 的结果,B 通过状态、通知等,来通知 A 或回调函数来处理。
先执行完同步任务,才会去执行异步任务。异步任务还会分为宏任务和微任务。
什么是宏任务?什么是微任务?
-
微任务:一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前。
-
宏任务:宏任务的时间粒度比较大,执行的时间间隔是不能精确控制的,对一些高实时性的需求就不太符合。
常见微任务:
Promise.then- MutaionObserver
- Object.observe (已废弃)
- process.nextTick ( Node.js )
常见宏任务:
<script>(可以理解为外层同步代码)setTimeout/setInterval- UI rendering / UI 事件
- postMessage,NessageChannel
- setImediate ( Node.js )、I/O
事件循环面试题
console.log(1)
let p = new Promise(resolve => {
console.log(2)
resolve(3)
})
p.then((res) => {
console.log(res)
})
setTimeout(() => {
console.log(4)
}, 0)
console.log(5)
// 1->2->5->3->4
解析:
- 首先,事件循环从宏任务队列开始,最初始,宏任务队列中,只有一个 script(整体代码)任务;
- 遇到了console.log,输出 1;
- 接着往下走,遇到 promise,new promise 中的代码立即执行,输出 2,然后执行 resolve ;
- 将其 then 分发到微任务队列中去,记为 then1;
- 遇到 setTimeout ,将其分发到任务队列中去,记为 timemout1;
- 接着遇到 console.log 代码,直接输出 5;至此,当前宏任务执行完毕。
- 接着检查微任务队列,发现有个 then1 微任务,执行,输出 3;
- 再检查微任务队列,发现已经清空,则开始检查宏任务队列;
- 执行 timeout1,输出 4;
- 至此,所有的队列都已清空,执行完毕。 其输出的顺序依次是:1->2->5->3->4。
扩展:Promise 平行链
Promise.resolve().then(() => {
console.log(0);
return Promise.resolve(4);
}).then((res) => {
console.log(res)
})
Promise.resolve().then(() => {
console.log(1);
}).then(() => {
console.log(2);
}).then(() => {
console.log(3);
}).then(() => {
console.log(5);
}).then(() =>{
console.log(6);
})
// 0->1->2->3->4->5->6
js里的所有对象都有原型吗?
__proto__ 和 prototype 之间有什么关系
__proto__ 实例化对象,prototype (原型属性)构造函数/类
答案:所有对象都有 __proto__ 属性,函数这个特殊对象除了具有 __proto__ 属性,还有特有的原型属性 prototype 。 prototype 对象默认有两个属性,constructor 属性和 __proto__ 属性。 prototype 属性可以给函数和对象添加可共享(继承)的方法、属性,而 __proto__ 是查找某函数或对象的原型链方式。 constructor,这个属性包含了一个指针,指回原构造函数。
节流和防抖分别是什么?在什么场景下使用?请分别实现一个节流函数和一个防抖函数?
答案:防抖( debounce )就是在事件触发后的 n 秒之后,再去执行真正需要执行的函数,如果在这 n 秒之内事件又被触发,则重新开始计时(不再触发,才去执行)。防抖函数实现如下:
const debounce = (fn , delay = 500) => {
let timer ;
return function (...args){
clearTimeout(timer) // 清空定时器
timer = setTimeout( function () (
fn.apply(this, args); // 改变 this 指向
), delay ) // 延迟一定时间执行
}
}
节流( throttle )就是规定好一个单位时间,触发函数一次。如果在这个单位时间内触发多次函数的话(高频率),只有一次是可被执行的。想执行多次的话,只能等到下一个周期里。实现代码如下:
// 定时器版本,事件触发延后
const trottle1 = (fn, delay = 500) => {
let timer
return (...args) => {
if (!timer){
timer = setTimeout(() => {
clearTimeout(timer)
timer = null;
fn.apply(this, args);
}, delay );
}
}
}
// 时间戳版本,立即执行
const trottle2 = (fn, delay = 500) => {
let oldTime = Date.now();
return (...args) => {
let nowTime = Date.now();
if (nowTime - oldTime >= delay){
oldTime = Date.now();
fn.apply(this, args);
}
}
}
实际运用中比如,按键高频率重复触发,拖拽场景、表单验证场景 resize, scroll 、 onmousemove 等等触发事件。
事件流
什么是事件流∶事件流描述的是从页面中接收事件的顺序。
事件发生时会在元素节点与根节点之间按照特定的顺序传播,路径所经过的所有节点都会收到该事件,这个传播过程即DOM事件流。
事件传播的顺序对应浏览器的两种事件流模型:捕获型事件流和冒泡型事件流。
- 冒泡型事件流:事件的传播是从最特定的事件目标到最不特定的事件目标。即从DOM树的叶子到根。
- 捕获型事件流:事件的传播是从最不特定的事件目标到最特定的事件目标。即从DOM树的根到叶子。
事件冒泡
一个事件触发后,会在子元素和父元素之间传播,这种传播分为三个阶段:
- 捕获阶段(从 window 对象传导到目标节点(从外到里),这个阶段不会响应任何事件)
- 目标阶段,(在目标节点上触发)
- 冒泡阶段(从目标节点传导回 window 对象(从里到外))
事件委托是什么?
事件委托就是利用事件冒泡,只制定一个时间处理程序,就可以管理某一类型的所有事件。
将原本需要绑定在子元素的响应事件委托给父元素,让父元素担当事件监听的职务。
如何确定事件源? Event.target 谁调用谁就是事件源。
什么是纯函数?使用纯函数有什么好处?
答案:纯函数是对给定的输入返还相同输出的函数。纯函数在函数式编程中被大量使用,在前端如 reactjs 、 redux 等库中大量被使用。
例如
let double = value => value * 2 函数只依赖外部变量 value 来影响 double 的值,每次给定的输入会有相同的输出。假如:
let num = 2
let double = value => num * 2;
如上函数就不是一个纯函数, double 值会受到外部 num 值的影响,导致 double 函数出现副作用(副作用:一个函数执行过程产生了外部可观察的变化)。
纯函数好处:
- 可以产生可测试的代码。如下:
test('double(2)等于4', () => {
expect(double(2)).toBe(4);
})
-
可读性更强。
-
可缓存,通过缓存 缓存执行结果,如下:
function memoize (fn) {
let cache = [];
return function ({
cache.push(fn.apply(this, arguments));
return cache;
}
}
let addFn = memoize (function (a, b) {
return a + b;
})
console.log(addFn(1, 2)); // [3]
console.log(addFn(2, 2)); // [3, 4]
闭包
外部参数在内部 return 方法中引用,捆绑在一起形成闭包。
本质官方解答:函数在执行的时候会放在栈上执行,当函数执行完毕了,就从栈中移除,但如果外部函数的参数在内部方法进行引用,则无法释放,因此形成了闭包。
function mackPower(power){
return function (x) {
return power * x
}
}
let power2 = mackPower(2)
let power3 = mackPower(3)
console.log( power2(2) ) // 4
console.log( power3(3) ) // 9
特点∶
- 函数嵌套函数。
- 函数内部可以引用外部的参数和变量。
- 参数和变量不会被垃圾回收机制回收。
优点∶
- 变量长期驻在内存中;
- 避免全局变量的污染;
- 私有化变量;
- 模拟块级作用域
缺点∶
- 会导致函数的变量一直保存在内存中,过多的闭包可能会导致内存泄露
高阶函数
函数作为参数/返回值的函数就叫高阶函数。
函数柯里化 实现 add(1)(2)(3)
答案:考察函数柯里化,柯里化( Currying )是把接受多个参数的函数转变为接受一个单一参数的函数,并且返回接受余下的参数且返回结果的新函数的技术。
function add (a){
return function (b){
return function (c){
return a + b + c ;
}
}
}
console.log(add(1)(2)(3)); // 6
当然也会有多参数的柯里化:
function add = function (a, b, c){
return a + b + c
}
const curry = function (fn){
return function curriedFn(...args){
if(args.length < fn.length ){
return function (){
return curry.apply(null, args.concat(Array.from(arguments))) // arguments 是伪数组,需要转化
// return curry(...args.concat(Array.from(arguments)))
}
}
return fn.apply(this, args);
// return fn(...args);
}
}
const myadd = curry(add)
let res = myAdd(1)(2)(3)
console.log(res) // 6
柯里化作用:1、参数复用。2、延迟执行。3、某些语言及特定环境下只能接受一个参数。
函数式编程中的 compose (*)
答案:compose 是函数式编程中一个非常重要的函数, compose 的函数作用就是组合函数的,将函数串联起来执行。将多个函数组合起来,一个函数的输出结果是另一个函数的输入参数(从右至左),一旦第一个函数开始执行,就会像多米诺骨牌一样推导执行了。
// reverse 函数倒置执行 reduce 累加
const compose = (...fns) => {
return (arg) => {
return fns.reverse().reduce((acc, fn) => {
return fn(acc) // acc 函数运行结果累加值 fn 当前函数 arg 初始值
}, arg);
}
}
// 简写
// const compose = (...fns) => arg => fns.reverse().reduce((acc, fn) => fn(acc), arg);
let aFn = a => a * 2
let bFn = b => b + 3
let myFn = compose(aFn, bFn)
let res = myFn(2)
console.log(res) // 10
按要求实现 go 函数
示例:
go('1') // 'go1'
go()('1') // 'goo1'
go()()('1') // 'gooo1'
答案:这里主要是高阶函数和闭包及递归的运用,具体代码如下:
function go (...args) {
let o = 'o'
const fn = (...args) => {
if (typeof args[0] === 'undefined') {
return (...args) => {
o += 'o'
return fn(...args)
}
} else {
return 'g' + o + args[0]
}
}
return fn(...args)
}
let res = go()('1')
console.log(res) // 'goo1'
实现一个 myMap 函数,实现类似 map 功能?
答案:这个是通过声明式编程(结果)方式抽象循环逻辑,代码如下:
命令式编程(过程)
Array.prototype.myMap = function (fn) {
let resultArr = []
for(let i = 0; i < this.length; i++){
resultArr.push(fn(this[i], i, this))
}
return resultArr
}
let arr = ['张三', '李四', '王五']
let res = arr.myMap((item, key, arr) => {
console.log(item, key, arr)
})
console.log(res) // ['张三', '李四', '王五']
数组去重(*)
function uniqueArr(arr) {
return [...new Set(arr)];
}
数组扁平化(*)
数组扁平化:使多维数组变成一维数组。
- 递归版:
function flatter(arr) {
if (!arr.length) return;
return arr.reduce(
(pre, cur) =>
Array.isArray(cur) ? [...pre, ...flatter(cur)] : [...pre, cur],
[]
);
}
// console.log(flatter([1, 2, [1, [2, 3, [4, 5, [6]]]]]));
- 迭代版:
function flatter(arr) {
if (!arr.length) return;
while (arr.some((item) => Array.isArray(item))) {
arr = [].concat(...arr);
}
return arr;
}
// console.log(flatter([1, 2, [1, [2, 3, [4, 5, [6]]]]]));
深拷贝(*)
// 深拷贝
function deepCopy(obj) {
let newObj = Array.isArray(obj) ? [] : {}; // 可能有数组
for(let key in obj){ // 遍历对象。for...in 不仅会循环对象自身的属性和方法,还会循环对象的原型以及原型链上的属性和方法。
if(obj.hasOwnProperty(key)){ // 判断是否是对象的自身属性和方法
if(typeof obj[key] === "object"){ // 判断是否是对象
newObj[key] = deepCopy(obj[key]) ; // 再次循环
}else{ // 非对象,简单数据类型
newObj[key] = obj[key];
}
}
}
return newObj;
}
Js中常见的内存泄漏
- 意外的全局变量;
- 被遗忘的计时器或回调函数;
- 脱离 DOM的引用;
- 闭包;
内存泄漏是指一块被分配的内存既不能使用又不能回收,直到浏览器进程结束。
释放内存的方法∶赋值为 null;
JS 监听对象属性的改变
- 在 ES5 中可以通过Object.defineProperty 来实现已有属性的监听:
Object.defineProperty(user,'name',{
set: function(key,value){}
})
缺点∶如果id 不在user 对象中,则不能监听 id 的变化。
- 在ES6 中可以通过 Proxy 来实现:
Var user = new Proxy({},{
set: function(target,key,value,receiver){}
})
这样即使有属性在user 中不存在,通过 user.id 来定义也同样可以这样监听这个属性的变化哦。
Commonjs ES6模块区别?
common模块是拷贝,可以修改值,es6 模块是引用,只读状态,不能修改值;
commonjs 模块是运行时加载,es6 模块是编译时输出接口;
JS 的各种位置,比 clientHeight,scrollHeight,offsetHeight,以及scrollTop, offsetTop,clientTop 的区别?
clientHeight∶表示的是可视区域的高度,不包含 border 和滚动条;
offsetHeight∶表示可视区域的高度,包含了 border 和滚动条;
scrollHeight∶表示了所有区域的高度,包含了因为滚动被隐藏的部分;
clientTop∶表示边框 border 的厚度,在未指定的情况下一般为0;
scrollTop∶滚动后被隐藏的高度,获取对象相对于由 offsetParent 属性指定的父坐标(css定位的元素或 body 元素距离顶端的高度。
JS 拖拽功能的实现
首先是三个事件,分别是mousedown,mousemove,mouseup当鼠标点击按下的时候,需要一个 tag 标识此时已经按下,可以执行mousemove 里面的具体方法。clientX,clientY 标识的是鼠标的坐标,分别标识横坐标和纵坐标,并且我们用 offsetX 和 offsetY 来表示元素的元素的初始坐标,移动的举例应该是∶鼠标移动时候的坐标-鼠标按下去时候的坐标。也就是说定位信息为; 鼠标移动时候的A标-鼠标按下去时候的坐标+元素初始情况下的 offetLef.还有一点也是原理性的东西,也就是拖拽的同时是绝对定位,我们改变的是绝对定位条件下的 left 以及 top 等等值。
补充∶也可以通过 html5 的拖放(Drag 和 drop)来实现
JS 中的垃圾回收机制
必要性∶由于字符串、对象和数组没有固定大小,所以当他们的大小已知时,才能对他们进行动态的存储分配。JavaScript 程序每次创建字符串、数组或对象时,解释器都必须分配内存来存储那个实体。只要像这样动态地分配了内存,最终都要释放这些内存以便他们能够被再用,否则,JavaScript 的解释器将会消耗完系统中所有可用的内存,造成系统崩溃。
这段话解释了为什么需要系统需要垃圾回收,JS 不像C/C++,他有自己的一套垃圾回收机制(Garbage Collection)。JavaScript 的解释器可以检测到何时程序不再使用一个对象了,当他确定了一个对象是无用的时候,他就知道不再需要这个对象,可以把它所占用的内存释放掉了。
例如∶
var a="hello world";
var b="world";
Var a=b; // 这时,会释放掉"hello world"
垃圾回收的方法∶标记清除、计数引用。
- 标记清除
这是最常见的垃圾回收方式,当变量进入环境时,就标记己这个变量为"进入环境",从逻辑上讲,永远不能释放进入环境的变量所占的内存,永远不能释放进入环境变量所占用的内存,只要执行流程进入相应的环境,就可能用到他们。当离开环境时,就标记为离开环境。
垃圾回收器在运行的时候会给存储在内存中的变量都加上标记(所有都加),然后去掉环境变量中的变量,以及被环境变量中的变量所引用的变量(条件性去除标记),删除所有被标记的变量,删除的变量无法在环境变量中被访问所以会被删除,最后垃圾回收器,完成了内存的清除工作,并回收他们所占用的内存。
- 引用计数法
引用计数法的意思就是每个值没引用的次数,当声明了一个变量,并用一个引用类型的值赋值给改变量,则这个值的引用次数为 1;相反的,如果包含了对这个值引用的变量又取得了另外一个值,则原先的引用值引用次数就减 1,当这个值的引用次数为 0 的时候,说明没有办法再访问这个值了,因此就把所占的内存给回收进来,这样垃圾收集器再次运行的时候,就会释放引用次数为 0 的这些值。
用引用计数法会存在内存泄露,下面来看原因∶
function problem() {
var objA= new Object();
var objB= new Object();
objA.someOtherObject = objB;
objB.anotherObject =objA;
}
在这个例子里面,objA 和 objB 通过各自的属性相互引用,这样的话,两个对象的引用次数都为 2,在采用引用计数的策略中,由于函数执行之后,这两个对象都离开了作用域,函数执行完成之后,因为计数不为 0,这样的相互引用如果大量存在就会导致内存泄露。
特别是在 DOM 对象中,也容易存在这种问题∶
var element=document.getElementByld('.);
var myObj=new Object();
myObj.element=element;
element.someObject=myObj;
这样就不会有垃圾回收的过程。
发布/订阅模式和观察者模式的区别是什么
观察者模式中主体和观察者是互相感知的,发布-订阅模式是借助第三方来实现调度的,发布者和订阅者之间互不感知