菜鸡打卡
1.JS 块级作用域、变量提升
- 块级作用域
JS 中作用域有:全局作用域、函数作用域。没有块作用域的概念。ECMAScript 6(简称 ES6)中新增了块级作用域。
块作用域由 { } 包括,if 语句和 for 语句里面的{ }也属于块作用域。
- 变量提升
- 如果变量声明在函数里面,则将变量声明提升到函数的开头
- 如果变量声明是一个全局变量,则将变量声明提升到全局作用域的开头
解析
{
var a = 1;
console.log(a); // 1
} console.log(a); // 1
// 可见,通过var定义的变量可以跨块作用域访问到。
(function A() {
var b = 2;
console.log(b); // 2
})();
// console.log(b); // 报错,
// 可见,通过var定义的变量不能跨函数作用域访问到
if (true) {
var c = 3;
} console.log(c); // 3
for (var i = 0; i < 4; i++) {
var d = 5;
}
console.log(i); // 4 (循环结束i已经是4,所以此处i为4)
console.log(d); // 5
// if语句和for语句中用var定义的变量可以在外面访问到,
// 可见,if语句和for语句属于块作用域,不属于函数作用域。
{
var a = 1;
let b = 2;
const c = 3;
{
console.log(a); // 1 子作用域可以访问到父作用域的变量
console.log(b); // 2 子作用域可以访问到父作用域的变量
console.log(c); // 3 子作用域可以访问到父作用域的变量
var aa = 11;
let bb = 22;
const cc = 33;
}
console.log(aa); // 11 // 可以跨块访问到子 块作用域 的变量
// console.log(bb); // 报错 bb is not defined
// console.log(cc); // 报错 cc is not defined
}
拓展
var、let、const的区别
- var 定义的变量,没有块的概念,可以跨块访问, 不能跨函数访问。
- let 定义的变量,只能在块作用域里访问,不能跨块访问,也不能跨函数访问。
- const 用来定义常量,使用时必须初始化(即必须赋值),只能在块作用域里访问,而且不能修改。
- 同一个变量只能使用一种方式声明,不然会报错
/ 块作用域
{
var a = 1;
let b = 2;
const c = 3;
// c = 4; // 报错
// let a = 'a'; // 报错 注:是上面 var a = 1; 那行报错
// var b = 'b'; // 报错:本行报错
// const a = 'a1'; // 报错 注:是上面 var a = 1; 那行报错
// let c = 'c'; // 报错:本行报错
var aa;
let bb;
// const cc; // 报错
console.log(a); // 1
console.log(b); // 2
console.log(c); // 3
console.log(aa); // undefined
console.log(bb); // undefined
}
console.log(a); // 1
// console.log(b); // 报错
// console.log(c); // 报错
// 函数作用域
(function A() {
var d = 5;
let e = 6;
const f = 7;
console.log(d); // 5
console.log(e); // 6 (在同一个{ }中,也属于同一个块,可以正常访问到)
console.log(f); // 7 (在同一个{ }中,也属于同一个块,可以正常访问到)
})();
// console.log(d); // 报错
// console.log(e); // 报错
// console.log(f); // 报错
2. null/undefined 的区别
null: Null 类型,代表“空值",代表一个空对象指针,使用 typeof 运算得到 “object",所以你可以认为它是一个特殊的对象值。
console.log(typeof null)//object
console.log(Object.prototype.toString.call(null))//[object Null]
undefined: Undefined 类型,当一个声明了一个变量未初始化时,得到的就是 undefined。
3.JS 哪些操作会造成内存泄露(了解)
- 意外的全局变量引起的内存泄露
function leak() {
leal = 'xxx';//leak成为一个全局变量,不会被回收
}
- 闭包引起的内存泄露
function bindEvent() {
var obj = document.createElement("XXX");
obj.οnclick = function() {
//Even if it's a empty function
}
}
闭包可以维持函数内局部变量,使其得不到释放。 上例定义事件回调时,由于是函数内定义函数,并且内部函数-事件回调的引用外暴了,形成了闭包。 解决之道,将事件处理函数定义在外部,解除闭包, 或者在定义事件处理函数的外部函数中,删除对 dom 的引用。
// 将事件处理函数定义在外部
function onclickHandler() {
// do something
}
function bindEvent() {
var obj = document.createElement('xxx')
obj.onclick = onclickHandler()
}
// 在定义事件处理函数的外部函数中,删除对dom的引用
function bindEvent() {
var obj document.createElement('xxx')
obj.onclick = function () {
//Even if it's a empty function
}
obj = null;
}
- 没有清理的 DOM 元素引用
var elements = {
button: document.getElementById('button'),
image: document.getElementById('image'),
text: document.getElementById('text'),
}
function doStuff() {
image.src = 'http://some.url/image';
button.click():
console.log(text.innerHTML)
}
function removeButton() {
document.body.removeChild(document.getElementById('button'))
}
- 被遗忘的定时器或者回调
var someResouce = getData();
setInterval(function() {
var node = document.getElementById("Node");
if (node) {
node.innerHTML = JSON.stringify(someResouce);
}
}, 1000);
这样的代码很常见, 如果 id 为 Node 的元素从 DOM 中移除, 该定时器仍会存在, 同时, 因为回调函数中包含对someResource 的引用, 定时器外面的 someResource 也不会被释放。
- 子元素存在引起的内存泄露
黄色是指直接被 js 变量所引用,在内存里,红色是指间接被 js 变量所引用,如上图,refB 被 refA 间接引用,导致即使 refB 变量被清空,也是不会被回收的子元素 refB 由于 parentNode 的间接引用,只要它不被删除,它所有的父元素(图中红色部分)都不会被删除。
- IE7/8 引用计数使用循环引用产生的问题
function fn() {
var a = {};
var b = {};
a.pro = b;
b.pro = a;
}
fn();
fn()执行完毕后,两个对象都已经离开环境,在标记清除方式下是没有问题的,但是在引用计数策略下,因为 a 和 b 的引用次数不为 0,所以不会被垃圾回收器回收内存,如果 fn 函数被大量调用,就会造成内存泄漏。在IE7 与IE8 上,内存直线上升。 IE 中有一部分对象并不是原生 js 对象。例如,其内存泄漏 DOM 和 BOM 中的对象就是使用 C++以 COM 对象的形式实现的,而 COM 对象的垃圾回收机制采用的就是引用计数策略。因此,即使 IE 的 js 引擎采用标记清除策略来实现,但 js 访问的 COM 对象依然是基于引用计数策略的。换句话说,只要在 IE 中涉及COM 对象,就会存在循环引用的问题。
var element = document.getElementById("some_element");
var myObject = new Object();
myObject.e = element;
element.o = myObject;
上面的例子在一个 DOM 元素(element)与一个原生 js 对象(myObject)之间创建了循环引用。其中,变量myObject 有一个名为 e 的属性指向 element 对象;而变量 element 也有一个属性名为 o 回指 myObject。由于存在这个循环引用,即使例子中的 DOM 从页面中移除,它也永远不会被回收。
看上面的例子,有人会觉得太弱了,谁会做这样无聊的事情,但是其实经常会这样做
window.οnlοad = function outerFunction() {
var obj = document.getElementById("element"):
obj.οnclick = function innerFunction() {};
}
这段代码看起来没什么问题,但是 obj 引用了document.getElementById(“element”),而 document. getElementById(“element”)的 onclick 方法会引用外部环境中的变量,自然也包括 obj,是不是很隐蔽啊。
简单的解决方式就是自己手工解除循环引用,比如刚才的函数可以这样
myObject.element = null;
element.o = null;
window.οnlοad = function outerFunction() {
var obj = document.getElementById("element"):
obj.οnclick = function innerFunction() {};
obj = null;
};
将变量设置为 null 意味着切断变量与它此前引用的值之间的连接。当垃圾回收器下次运行时,就会删除这些值并回收它们占用的内存。 要注意的是,IE9+并不存在循环引用导致 Dom 内存泄漏问题,可能是微软做了优化,或者Dom 的回收方式已经改变
解析
- JS 的回收机制
JavaScript 垃圾回收的机制很简单:找出不再使用的变量,然后释放掉其占用的内存,但是这个过程不是实时 的,因为其开销比较大,所以垃圾回收系统(GC)会按照固定的时间间隔, 周期性的执行。
到底哪个变量是没有用的?所以垃圾收集器必须跟踪到底哪个变量没用,对于不再有用的变量打上标记,以备将来收回其内存。用于标记的无用变量的策略可能因实现而有所区别,通常情况下有两种实现方式:标记清除和引用计数。引用计数不太常用,标记清除较为常用。
- 标记清除(mark and sweep)
js 中 常用的垃圾回收方式就是标记清除。当变量进入环境时,例如,在函数中声明一个变量,就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所用的内存,因为只要执行流进入相应的环境,就可能会用到它们。而当变量离开环境时,则将其标记为“离开环境”。
function test() {
var a = 10; //被标记,进入环境
var b = 20; //被标记,进入环境
}
test(); //执行完毕之后a、b又被标记离开环境,被回收
- 引用计数(reference counting)
引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型值(function object array)赋给该变量时,则这个值的引用次数就是 1。如果同一个值又被赋给另一个变量,则该值的引用次数加 1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减 1。当这个值的引用次数变成 0 时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。这样,当垃圾回收器下次再运行时,它就会释放那些引用次数为 0 的值所占用的内存。
function test() {
var a = {}; //a的引用次数为0
var b = a; //a的引用次数加1,为1
var c = a; //a的引用次数加1,为2
var b = {}; //a的引用次数减1,为1
}
- 如何分析内存的使用情况
Google Chrome 浏览器提供了非常强大的 JS 调试工具,Memory 视图 profiles 视图让你可以对 JavaScript 代码运行时的内存进行快照,并且可以比较这些内存快照。它还让你可以记录一段时间内的内存分配情况。在每一个结果视图中都可以展示不同类型的列表,但是对我们 有用的是 summary 列表和 comparison 列表。summary 视图提供了不同类型的分配对象以及它们的合计大小:shallow size (一个特定类型的所有对象的总和)和 retained size (shallow size 加上保留此对象的其它对象的大小)。distance 显示了对象到达 GC 根(校者注: 初引用的那块内存,具体内容可自行搜索该术语)的 短距离。 comparison 视图提供了同样的信息但是允许对比不同的快照。这对于找到泄漏很有帮助。
- 怎样避免内存泄露
- 减少不必要的全局变量,或者生命周期较长的对象,及时对无用的数据进行垃圾回收;
- 注意程序逻辑,避免“死循环”之类的 ;
- 避免创建过多的对象 原则:不用了的东西要及时归还。
4. 重排与重绘的区别,什么情况下会触发?
- 简述重排的概念
浏览器下载完页面中的所有组件(HTML、JavaScript、CSS、图片)之后会解析生成两个内部数据结构(DOM 树和渲染树),DOM 树表示页面结构,渲染树表示 DOM 节点如何显示。重排是 DOM 元素的几何属性变化,DOM 树的结构变化,渲染树需要重新计算。
- 简述重绘的概念
重绘是一个元素外观的改变所触发的浏览器行为,例如改变visibility、outline、背景色等属性。浏览器会根据元素的新属性重新绘制,使元素呈现新的外观。由于浏览器的流布局,对渲染树的计算通常只需要遍历一次就可以完成。但 table 及其内部元素除外,它可能需要多次计算才能确定好其在渲染树中节点的属性值,比同等元素要多花两倍时间,这就是我们尽量避免使用 table 布局页面的原因之一。
- 简述重绘和重排的关系
重绘不会引起重排,但重排一定会引起重绘,一个元素的重排通常会带来一系列的反应,甚至触发整个文档的重排和重绘,性能代价是高昂的。
- 什么情况下会触发重排?
- 页面渲染初始化时
- 浏览器窗口改变尺寸 (这个无法避免)
- 元素尺寸改变时
- 元素位置改变时
- 元素内容改变时
- 添加或删除可见的 DOM 元素时
- 重排优化有如下五种方法
-
将多次改变样式属性的操作合并成一次操作,减少 DOM 访问
-
如果要批量添加 DOM,可以先让元素脱离文档流,操作完后再带入文档流,这样只会触发一次重排。(fragment 元素的应用)
-
将需要多次重排的元素,position 属性设为 absolute 或 fixed,这样此元素就脱离了文档流,它的变化不会影响到其他元素。例如有动画效果的元素就 好设置为绝对定位。
-
由于 display 属性为 none 的元素不在渲染树中,对隐藏的元素操作不会引发其他元素的重排。如果要对一个元素进行复杂的操作时,可以先隐藏它,操作完成后再显示。这样只在隐藏和显示时触发两次重排。
-
在内存中多次操作节点,完成后再添加到文档中去。例如要异步获取表格数据,渲染到页面。可以先取得数据后在内存中构建整个表格的 html 片段,再一次性添加到文档中去,而不是循环添加每一行。
5. jsonp 优缺点?
jsonp
优点
-
它不像 XMLHttpRequest 对象实现的 Ajax 请求那样受到同源策略的限制,JSONP 可以跨越
-
同源策略:它的兼容性更好,在更加古老的浏览器中都可以运行,不需要 XMLHttpRequest 或 ActiveX 的支持
-
在请求完毕后可以通过调用 callback 的方式回传结果。将回调方法的权限给了调用方。这个就相当于将 controller 层和 view 层终于分 开了。我提供的 jsonp 服务只提供纯服务的数据,至于提供服务以后的页面渲染和后续 view 操作都由调用者来自己定义就好了。如果有两个页面需要渲染同一份数据,你们只需要有不同的渲染逻辑就可以了,逻辑都可以使用同一个 jsonp 服务
缺点
-
它只支持 GET 请求而不支持 POST 等其它类型的 HTTP 请求
-
它只支持跨域 HTTP 请求这种情况,不能解决不同域的两个页面之间如何进行 JavaScript 调用的问题
-
jsonp 在调用失败的时候不会返回各种 HTTP 状态码。
-
安全性也是它的缺点。万一假如提供 jsonp 的服务存在页面注入漏洞,即它返回的 javascript 的内容被人控制的。那么结果是什么?所有调用这个 jsonp 的网站都会存在漏洞。于是无法把危险控制在一个域名下…所以在使用 jsonp 的时候必须要保证使用的 jsonp 服务必须是安全可信的
6.兼容各种浏览器版本的事件绑定
/*
兼容低版本IE,ele为需要绑定事件的元素,
eventName为事件名(保持addEventListener语法,去掉on), fun为事件响应函数
*/
function addEvent(ele, eventName, fun) {
if (ele.addEventListener) {
ele.addEventListener(eventName, fun, false);
} else {
ele.attachEvent('on' + eventName, fun)
}
}
7. bind、call、apply 的区别
call 和 apply 其实是一样的,区别就在于传参时参数是一个一个传或者是以一个数组的方式来传。 call 和 apply 都是在调用时生效,改变调用者的 this 指向。
let name = 'Jack'
const obj = {name: 'Tom'}
function sayHi() {console.log('Hi! ' + this.name)}
sayHi() // Hi! Jack
sayHi.call(obj) // Hi! Tom
bind 也是改变 this 指向,不过不是在调用时生效,而是返回一个新函数。
const newFunc = sayHi.bind(obj)
newFunc() // Hi! Tom
8.如何封装一个 javascript 的类型判断函数?
function getType(value) {
// 判断数据是 null 的情况
if (value === null) {
return value + "";
}
// 判断数据是引用类型的情况
if (typeof value === "object") {
let valueClass = Object.prototype.toString.call(value),
type = valueClass.split(" ")[1].split("");
type.pop();
return type.join("").toLowerCase();
} else {
// 判断数据是基本数据类型的情况和函数的情况
return typeof value;
}
}
9.如何判断一个对象是否为空对象?
function checkNullObj(obj) {
return Object.keys(obj).length === 0 && Object.getOwnPropertySymbols(obj).length === 0;
}
10.发布订阅设计模式
发布—订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。在 JavaScript 开发中,我们一般用事件模型来替代传统的发布—订阅模式。
解析
发布订阅模式的构成
常见的发布订阅模式就是咱们 DOM 事件,仔细回想一下要给一个按钮,绑定一个事件,当我点击按钮的时候我要让他的颜色变了,并且页面弹出一个弹出框
分析一下这个流程:首先,得知道给哪个按钮的时候绑定事件,然后得知道触发事件以后需要干什么?
那么在这其中谁是发布者?
- 是 DOM 中的按钮,因为是在它身上绑定了事件,当我们点击按钮的时候它便向订阅者发布了这个消息那么谁是
订阅者?
- 是 click 事件,当点击按钮时,dom 发布了一条消息,而事件订阅了它,所以当它被点击的时候,订阅者会接收到消息
简单例子
上大学的时候甲要打游戏,下午的课不准备去了,然后甲跟乙说,如果下午老师问我去哪了,你就发信息告诉我。然后果然不出所料,下午老师来了,并且问了甲去哪了?然后乙发信息给甲,甲收到信息后干什么乙不知道,乙只是负责如果老师问了就发短信给甲。