blog's People
blog's Issues
移动端网页自适应方案
! function(a) {
function b() {
a.rem = f.getBoundingClientRect().width / 16, f.style.fontSize = a.rem + "px"
}
var c, d = a.navigator.appVersion.match(/iphone/gi) ? a.devicePixelRatio : 1,
e = 1 / d,
f = document.documentElement,
g = document.createElement("meta");
if (a.dpr = d, a.addEventListener("resize", function() {
clearTimeout(c), c = setTimeout(b, 300)
}, !1), a.addEventListener("pageshow", function(a) {
a.persisted && (clearTimeout(c), c = setTimeout(b, 300))
}, !1), f.setAttribute("data-dpr", d), g.setAttribute("name", "viewport"), g.setAttribute("content", "initial-scale=" + e + ", maximum-scale=" + e + ", minimum-scale=" + e + ", user-scalable=no"), f.firstElementChild) f.firstElementChild.appendChild(g);
else {
var h = document.createElement("div");
h.appendChild(g), document.write(h.innerHTML)
}
b()
}(window);
借鉴手淘的 flexible 方案:
-
ios 设备根据 dpr 计算缩放比,通过设置 meta 标签中 inital-scale 属性来达到缩放的目的;
-
andriod 设备由于兼容问题太过复杂,比如:
- viewport content 的写法要兼容各种奇葩 Android 设备,兼容测试的成本很高
- 部分机型修改viewport之后产生屏幕抖动
- 部分机型 WebView Width 与屏幕实际宽度不一致
- 还有很多无法预料到的奇葩BUG存在
所以统一对 android 设备进行 dpr=1 处理。然后通过设置根font-size 的方式使用 rem 进行适配。
nodejs 事件驱动和事件循环
nodejs 事件驱动和事件循环
nodejs是单线程(single thread)运行的,通过一个事件循环(event-loop)来循环取出消息队列(event-queue)中的消息进行处理,处理过程基本上就是去调用该消息对应的回调函数。消息队列就是当一个事件状态发生变化时,就将一个消息压入队列中。
nodejs的事件驱动模型一般要注意下面几个点:
- 因为是单线程的,所以当顺序执行js文件中的代码的时候,事件循环是被暂停的。
- 当js文件执行完以后,事件循环开始运行,并从消息队列中取出消息,开始执行回调函数
- 因为是单线程的,所以当回调函数被执行的时候,事件循环是被暂停的
- 当涉及到I/O操作的时候,nodejs会开一个独立的线程来进行异步I/O操作,操作结束以后将消息压入消息队列。
javascript 事件循环
javascript 有两个任务队列:
task
包括:script(整体代码),setTimeout,setInterval,setImmediate,I/O,UI rendering.
jobs
包括:process.nextTick,Promise,Object.observe(已废弃),MutationObserver(监视DOM变动的接口).
事件循环的顺序,决定了javascript代码的执行顺序。它从script(整体代码)开始第一次循环。之后全局上下文进入函数调用栈(消息队列?)。直到调用栈清空(只剩全局),然后执行所有的 jobs。当所有可执行的 jobs 执行完毕之后,循环再次从 task 开始,找到其中一个任务队列执行完毕,然后再执行所有的 jobs,这样一直循环下去。
basket.js 源码分析
一、前言
basket.js 可以用来加载js脚本并且保存到 LocalStorage 上,使我们可以更加精准地控制缓存,即使是在 http 缓存过期之后也可以使用。因此可以使我们防止不必要的重新请求 js 脚本,提升网站加载速度。
可以到 basket.js 的 Github 上查看更多的相关信息。
由于之前在工作中使用过 basket.js ,好奇它的实现原理,因此就有了这篇分析 basket.js 源码的文章。
二、简单的使用说明
basket.js 的使用非常简单,只要引入相应的js脚本,然后使用 basket 的 require 方法加载就可以了:
<!DOCTYPE html>
<html>
<head>
<title>basket.js demo</title>
<script src="basket.js"></script>
</head>
<body>
<script>
basket.require({
url: "/n/static/vendor-f8f4f260.js",
key: "/n/static/vendor.js",
unique: "f8f4f260",
},{
url: "/n/static/list-2f26cad1.js",
key: "/n/static/list.js",
unique: "2f26cad1",
});
</script>
</body>
</html>
第一次加载,页面 js 资源 vendor.js 和 list.js 会被保存到 LocalStorage:
此时对应的资源加载情况:
刷新一次页面,再查看一次资源的加载情况:
可以看到已经没有再发送 vender.js 和 list.js 相关的请求,因为 LocalStorage 上已经有对应的缓存了,直接从本地获取即可。
三、实现流程
流程图
细节说明
处理参数
处理参数就是根据已有的参数初始化未指定的参数。例如 require 方法支持 once 参数用来表示是否只执行一次对应 JS,execute 参数标示是否加载完该 JS 之后立刻执行。所以处理参数这一步骤就会根据是否执行过该 JS 和 once 参数是否为 ture 来设置execute参数。
获取缓存
调用 localStorage.getItem
方法获取缓存。存入的 key 值为 basket-
加上 JS 文件的 URL。以上面加载 vendor.js 为例,key 值为:basket-/n/static/vendor.js。获取的缓存为一个缓存对象,里面包含 JS 代码和相关的一些信息,例如:
{
"url": "/n/static/vendor-f8f4f260.js?basket-unique=f8f4f260",
"unique": "f8f4f260",
"execute": true,
"key": "/n/static/vendor.js",
"data": "xxxxxxxx",
"originalType": "application/javascript",
"type": "application/javascript",
"skipCache": false,
"stamp": 1459606005108,
"expire": 1477606005108
}
其中 data 属性对应的值就是 JS 代码。
判断缓存是否有效
判断比较简单,根据缓存对象里面的版本号 unique 和过期时间 expire 等来判断。这和浏览器使用 Expire 和 Etag 头部来判断 HTTP 缓存是否有效相似。最大的不同就是缓存完全由 JS 控制!这也就是 basket.js 最大的作用。让我们更好的控制缓存。默认的过期时间为5000小时,也就是208.33天。
判断代码:
/**
* 判断ls上的缓存对象是否过期
* @param {object} source 从ls里取出的缓存对象
* @param {object} obj 传入的参数对象
* @returns {Boolean} 过期返回true,否则返回false
*/
var isCacheValid = function(source, obj) {
return !source || // 没有缓存数据返回true
source.expire - +new Date() < 0 || // 超过过期时间返回true
obj.unique !== source.unique || // 版本号不同的返回true
(basket.isValidItem && !basket.isValidItem(source, obj)); // 自定义验证函数不成功的返回true
};
Ajax获取JS
普通的利用 XMLHttpRequest 请求。
缓存到localStorage
调用localStorage.setItem
方法保存缓存对象。一般来说,只要这一行代码就能完成本步骤。但是LocalStorage保存的数据是有大小限制的。以 chrome 为例,到了 5M 就会超出限制,浏览器抛出异常:
DOMException: Failed to execute 'setItem' on 'Storage': Setting the value of 'basket-/n/static/vendor.js' exceeded the quota
因此需要使用 try catch 对localStorage.setItem
方法进行异常捕获。当没容量不足时就需要根据保存时间逐一删除 LocalStorage 的缓存对象。
相关代码:
/**
* 把缓存对象保存到localStorage中
* @param {string} key ls的key值
* @param {object} storeObj ls的value值,缓存对象,记录着对应script的对象、有url、execute、key、data等属性
* @returns {boolean} 成功返回true
*/
var addLocalStorage = function( key, storeObj ) {
// localStorage对大小是有限制的,所以要进行try catch
// 5M就到了Chrome的极限
// 2.5M就带了移动设备的极限
// 超过之后会抛出如下异常:
// DOMException: Failed to execute 'setItem' on 'Storage': Setting the value of 'basket-/n/static/vendor.js' exceeded the quota
try {
localStorage.setItem( storagePrefix + key, JSON.stringify( storeObj ) );
return true;
} catch( e ) {
// localstorage容量不够,根据保存的时间删除已缓存到ls里的js代码
if ( e.name.toUpperCase().indexOf('QUOTA') >= 0 ) {
var item;
var tempScripts = [];
// 先把所有的缓存对象来出来,放到 tempScripts里
for ( item in localStorage ) {
if ( item.indexOf( storagePrefix ) === 0 ) {
tempScripts.push( JSON.parse( localStorage[ item ] ) );
}
}
// 如果有缓存对象
if ( tempScripts.length ) {
// 按缓存时间升序排列数组
tempScripts.sort(function( a, b ) {
return a.stamp - b.stamp;
});
// 删除缓存时间最早的js
basket.remove( tempScripts[ 0 ].key );
// 删除后在再添加,利用递归完成
return addLocalStorage( key, storeObj );
} else {
// no files to remove. Larger than available quota
// 已经没有可以删除的缓存对象了,证明这个将要缓存的目标太大了。返回undefined。
return;
}
} else {
// some other error
// 其他的错误,例如JSON的解析错误
return;
}
}
};
生成script标签注入带页面
生成 script 标签,append 到 document.head:
var injectScript = function( obj ) {
var script = document.createElement('script');
script.defer = true;
// Have to use .text, since we support IE8,
// which won't allow appending to a script
script.text = obj.data;
head.appendChild( script );
};
四、异步编程
basket.js 是一个典型的需要大量异步编程的库,所以稍有不慎,代码将会高度耦合,臃肿难看。。。
所以原版 basket.js 引入遵从 Promises/A+ 标准的异步编程库 RSVP.js 来这个问题。
为了减小引入的文件的大小,我们改写了 basket.js ,去掉了依赖的 RSVP.js ,改用ES5的方式实现了类似 Promise 的功能。
所以 basket.js 中涉及异步编程的方法都会返回一个 Promise 对象。很好地解决了异步编程问题。例如 basket.require 方法就是返回一个promise 对象,因此需要按顺序加载 JS 的时候可以这样子写:
basket.require({
url: 'helloworld.js'
}).then(function() {
basket.require({
url: 'helloworld2.js'
})
});
存在问题
-
其实 basket.js 算是一种黑科技,使用起来有比较多的东西要注意。例如强制刷新页面也不能清除 localStorage 上的缓存,所以每次修改代码时我们都需要手动清除 localStorage,比较麻烦。当然调试时可以在 JS 文件的头部添加localStorage.clear()解决这个问题。
-
还有就是 basket.js 已经好久没有更新了,毕竟黑科技,总会被时代淘汰。而且 api 文档也不齐全。
-
存在XSS安全隐患。localStorage中的信息,客户端是可以任意修改的。如果哪个黑客想练手一下,可以任意注入js代码。那么,在页面刷新的时候,注入的代码也将会被执行。
-
经实际测试,通过 localstorage 缓存静态资源性能提升有限,去缓存中取数据的耗时加上通过 eval 执行或者 new Function 的方式执行代码的耗时已经跟网络请求的耗时差不多了。但也给我们提供了思路,localstotage 可以用来缓存页面首屏接口不常变化的数据,来加快首屏的渲染。
源码完整注释
/*!
* basket.js
* v0.5.2 - 2015-02-07
* http://addyosmani.github.com/basket.js
* (c) Addy Osmani; License
* Created by: Addy Osmani, Sindre Sorhus, Andrée Hansson, Mat Scales
* Contributors: Ironsjp, Mathias Bynens, Rick Waldron, Felipe Morais
* Uses rsvp.js, https://github.com/tildeio/rsvp.js
*/(function( window, document ) {
'use strict';
var head = document.head || document.getElementsByTagName('head')[0];
var storagePrefix = 'basket-'; // 保存localStorage时的前缀
var defaultExpiration = 5000; // 默认过期时间为5000小时
var inBasket = []; // 保存已经执行过的js的url。辅助设置参数的execute选项。
/**
* 把缓存对象保存到localStorage中
* @param {string} key ls的key值
* @param {object} storeObj ls的value值,缓存对象,记录着对应script的对象、有url、execute、key、data等属性
* @returns {boolean} 成功返回true
*/
var addLocalStorage = function( key, storeObj ) {
// localStorage对大小是有限制的,所以要进行try catch
// 2.5M就带了移动设备的极限
// 5M就到了Chrome的极限
// 超过之后会抛出如下异常:
// DOMException: Failed to execute 'setItem' on 'Storage': Setting the value of 'basket-http://file.com/ykq/wap/v3Templates/timeout/timeout/large.js' exceeded the quota
try {
localStorage.setItem( storagePrefix + key, JSON.stringify( storeObj ) );
return true;
} catch( e ) {
// localstorage容量不够,根据保存的时间删除已缓存到ls里的js代码
if ( e.name.toUpperCase().indexOf('QUOTA') >= 0 ) {
var item;
var tempScripts = [];
// 先把所有的缓存对象来出来,放到 tempScripts里
for ( item in localStorage ) {
if ( item.indexOf( storagePrefix ) === 0 ) {
tempScripts.push( JSON.parse( localStorage[ item ] ) );
}
}
// 如果有缓存对象
if ( tempScripts.length ) {
// 按缓存时间升序排列数组
tempScripts.sort(function( a, b ) {
return a.stamp - b.stamp;
});
// 删除缓存时间最早的js
basket.remove( tempScripts[ 0 ].key );
// 删除后在再添加,利用递归完成
return addLocalStorage( key, storeObj );
} else {
// no files to remove. Larger than available quota
// 已经没有可以删除的缓存对象了,证明这个将要缓存的目标太大了。返回undefined。
return;
}
} else {
// some other error
// 其他的错误,例如JSON的解析错误
return;
}
}
};
/**
* 利用ajax获取相应url的内容
* @param {string} url 请求地址
* @returns {object} 返回promise对象,解决时的参数为对象:{content:'', type: ''}
*/
var getUrl = function( url ) {
var promise = new RSVP.Promise( function( resolve, reject ){
var xhr = new XMLHttpRequest();
xhr.open( 'GET', url );
xhr.onreadystatechange = function() {
if ( xhr.readyState === 4 ) {
if ( ( xhr.status === 200 ) ||
( ( xhr.status === 0 ) && xhr.responseText ) ) {
resolve( {
content: xhr.responseText,
type: xhr.getResponseHeader('content-type')
} );
} else {
reject( new Error( xhr.statusText ) );
}
}
};
// By default XHRs never timeout, and even Chrome doesn't implement the
// spec for xhr.timeout. So we do it ourselves.
// 自定义超时设置
setTimeout( function () {
if( xhr.readyState < 4 ) {
xhr.abort();
}
}, basket.timeout );
xhr.send();
});
return promise;
};
/**
* 获取js,保存缓存对象到ls
* @param {object} obj basket.require的参数对象(之前的处理过程中添加相应的属性)
* @returns {object} promise对象
*/
var saveUrl = function( obj ) {
return getUrl( obj.url ).then( function( result ) {
var storeObj = wrapStoreData( obj, result );
if (!obj.skipCache) {
addLocalStorage( obj.key , storeObj );
}
return storeObj;
});
};
/**
* 进一步添加对象obj属性
* @param {object} obj basket.require的参数(之前的处理过程中添加相应的属性)
* @param {object} data 包含content和type属性的对象,content就是js的内容
* @returns {object} 经过包装后的obj
*/
var wrapStoreData = function( obj, data ) {
var now = +new Date();
obj.data = data.content;
obj.originalType = data.type;
obj.type = obj.type || data.type;
obj.skipCache = obj.skipCache || false;
obj.stamp = now;
obj.expire = now + ( ( obj.expire || defaultExpiration ) * 60 * 60 * 1000 );
return obj;
};
/**
* 判断ls上的缓存对象是否过期
* @param {object} source 从ls里取出的缓存对象
* @param {object} obj 传入的参数对象
* @returns {Boolean} 过期返回true,否则返回false
*/
var isCacheValid = function(source, obj) {
return !source || // 没有缓存数据返回true
source.expire - +new Date() < 0 || // 超过过期时间返回true
obj.unique !== source.unique || // 版本号不同的返回true
(basket.isValidItem && !basket.isValidItem(source, obj)); // 自定义验证函数不成功的返回true
};
/**
* 判断缓存是否还生效,获取js,保存到ls
* @param {object} obj basket.require参数对象
* @returns {object} 返回promise对象
*/
var handleStackObject = function( obj ) {
var source, promise, shouldFetch;
if ( !obj.url ) {
return;
}
obj.key = ( obj.key || obj.url );
source = basket.get( obj.key );
obj.execute = obj.execute !== false;
shouldFetch = isCacheValid(source, obj); // 判断缓存是否还有效
// 如果shouldFetch为true,请求数据,保存到ls(live选项意义不明,文档也没有说,这里当它一只是undefined)
if( obj.live || shouldFetch ) {
if ( obj.unique ) {
// set parameter to prevent browser cache
obj.url += ( ( obj.url.indexOf('?') > 0 ) ? '&' : '?' ) + 'basket-unique=' + obj.unique;
}
promise = saveUrl( obj ); // 请求对应js,缓存到ls里
if( obj.live && !shouldFetch ) {
promise = promise
.then( function( result ) {
// If we succeed, just return the value
// RSVP doesn't have a .fail convenience method
return result;
}, function() {
return source;
});
}
} else {
// 缓存可用。
source.type = obj.type || source.originalType;
source.execute = obj.execute;
promise = new RSVP.Promise( function( resolve ){
// 下面的setTimeout用来解决结合requirejs使用时的加载问题。
// setTimeout(function(){
debugger;
resolve( source );
// },0);
});
}
return promise;
};
/**
* 把script插入到head中
* @param {object} obj 缓存对象
*/
var injectScript = function( obj ) {
var script = document.createElement('script');
script.defer = true;
// Have to use .text, since we support IE8,
// which won't allow appending to a script
script.text = obj.data;
head.appendChild( script );
};
// 保存着特定类型的执行函数,默认行为是把script注入到页面
var handlers = {
'default': injectScript
};
/**
* 执行缓存对象对应回调函数,把script插入到head中
* @param {object} obj 缓存对象
* @returns {undefined} 不需要返回结果
*/
var execute = function( obj ) {
// 执行类型特定的回调函数
if( obj.type && handlers[ obj.type ] ) {
return handlers[ obj.type ]( obj );
}
// 否则执行默认的注入script行为
return handlers['default']( obj ); // 'default' is a reserved word
};
/**
* 批量执行缓存对象动作
* @param {Array} resources 缓存对象数组
* @returns {Array} 返回参数resources
*/
var performActions = function( resources ) {
return resources.map( function( obj ) {
if( obj.execute ) {
execute( obj );
}
return obj;
} );
};
/**
* 处理请求对象,不包括执行对应的动作
* @param {object} 会把basket.require的参数传过来,也就是多个对象
* @returns {object} promise对象
*/
var fetch = function() {
var i, l, promises = [];
for ( i = 0, l = arguments.length; i < l; i++ ) {
promises.push( handleStackObject( arguments[ i ] ) );
}
return RSVP.all( promises );
};
/**
* 包装promise的then方法实现链式调用
* @returns {Object} 添加了thenRequire方法的promise实例
*/
var thenRequire = function() {
var resources = fetch.apply( null, arguments );
var promise = this.then( function() {
return resources;
}).then( performActions );
promise.thenRequire = thenRequire;
return promise;
};
window.basket = {
require: function() { // 参数为多个请求相关的对象,对象的属性:url、key、expire、execute、unique、once和skipCache等
// 处理execute参数
for ( var a = 0, l = arguments.length; a < l; a++ ) {
arguments[a].execute = arguments[a].execute !== false; // execute 默认选项为ture
// 如果有只执行一次的选项once,并之前已经加载过这个js,那么设置execute选项为false
if ( arguments[a].once && inBasket.indexOf(arguments[a].url) >= 0 ) {
arguments[a].execute = false;
// 需要执行的请求的url保存到inBasket,
} else if ( arguments[a].execute !== false && inBasket.indexOf(arguments[a].url) < 0 ) {
inBasket.push(arguments[a].url);
}
}
var promise = fetch.apply( null, arguments ).then( performActions );
promise.thenRequire = thenRequire;
return promise;
},
remove: function( key ) {
localStorage.removeItem( storagePrefix + key );
return this;
},
// 根据key值获取对应ls的value
get: function( key ) {
var item = localStorage.getItem( storagePrefix + key );
try {
// 这里测试过不解析,然后直接eval执行,测试发现性能并没有什么提升
return JSON.parse( item || 'false' );
} catch( e ) {
return false;
}
},
// 批量清除缓存对象,传入true只清除过期对象
clear: function( expired ) {
var item, key;
var now = +new Date();
for ( item in localStorage ) {
key = item.split( storagePrefix )[ 1 ];
if ( key && ( !expired || this.get( key ).expire <= now ) ) {
this.remove( key );
}
}
return this;
},
isValidItem: null, // 可以自己扩展一个isValidItem函数,来自定义判断缓存是否过期。
timeout: 5000, // ajax 默认的请求timeout为5s
// 添加特定类型的执行函数
addHandler: function( types, handler ) {
if( !Array.isArray( types ) ) {
types = [ types ];
}
types.forEach( function( type ) {
handlers[ type ] = handler;
});
},
removeHandler: function( types ) {
basket.addHandler( types, undefined );
}
};
// delete expired keys
// basket.js 加载时会删除过期的缓存
// 高耗时操作!!性能好的手机耗时50ms,性能差的手机耗时200ms!!
// 用版本号管理本地缓存,可以不做此操作!!
basket.clear( true );
})( this, document );
从两道题重新认识JS的this、作用域、闭包、对象
最近在网上看到了两道关于 this 指向的题,个人感觉对理解 this 的作用域还是很有帮助的,现贴出来一起学习下。
/**
* Question 1
*/
var name = 'window'
var person1 = {
name: 'person1',
show1: function () {
console.log(this.name)
},
show2: () => console.log(this.name),
show3: function () {
return function () {
console.log(this.name)
}
},
show4: function () {
return () => console.log(this.name)
}
}
var person2 = { name: 'person2' }
person1.show1()
person1.show1.call(person2)
person1.show2()
person1.show2.call(person2)
person1.show3()()
person1.show3().call(person2)
person1.show3.call(person2)()
person1.show4()()
person1.show4().call(person2)
person1.show4.call(person2)()
/**
* Question 2
*/
var name = 'window'
function Person (name) {
this.name = name;
this.show1 = function () {
console.log(this.name)
}
this.show2 = () => console.log(this.name)
this.show3 = function () {
return function () {
console.log(this.name)
}
}
this.show4 = function () {
return () => console.log(this.name)
}
}
var personA = new Person('personA')
var personB = new Person('personB')
personA.show1()
personA.show1.call(personB)
personA.show2()
personA.show2.call(personB)
personA.show3()()
personA.show3().call(personB)
personA.show3.call(personB)()
personA.show4()()
personA.show4().call(personB)
personA.show4.call(personB)()
大致意思就是,有两个对象 person1,person2,然后花式调用 person1 中的4个 show 方法,预测真正的输出。
日常开发中,我们经常用到 this。例如用 Jquery 绑定事件时,this 指向触发事件的 DOM 元素;编写 Vue 组件时, this 指向组件本身。对于新手来说,常会用一种意会的感觉去判断 this 的指向。以至于当遇到复杂的函数调用时,就分不清 this 的真正指向。
网上讲解 this 的帖子,一般都会提到这句话:this 总是指向调用该函数的对象。
然后 ES6 箭头函数并不是如此,大家会看到如下说法:
- 箭头函数的 this 指向外层函数作用域中的 this。
- 箭头函数 this 是定义函数时所在上下文中的 this。
- 箭头函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象。
说法很多,多读几遍理解一下发现说的都差不多。凭着对这些说法的理解做下上面的题吧。
分割线下公布答案:
题目1正确答案如下:
person1.show1() // person1
person1.show1.call(person2) // person2
person1.show2() // window
person1.show2.call(person2) // window
person1.show3()() // window
person1.show3().call(person2) // person2
person1.show3.call(person2)() // window
person1.show4()() // person1
person1.show4().call(person2) // person1
person1.show4.call(person2)() // person2
是否跟你的答案一样呢?用刚刚的理论分析一下:
person1.show1()
与person1.show1.call(person2)
好理解,验证了谁调用此方法,this 就是指向谁。
person1.show2()
与person1.show2.call(person2)
的结果用上面的定义解释,就开始让人不理解了。
它的执行结果说明 this 指向的是 window。那就不是所谓的定义时所在的对象。
如果说是外层函数作用域中的 this,实际上并没有外层函数了,外层就是全局环境了,这个说法也不严谨。
只有定义函数时所在上下文中的this这句话算能描述现在这个情况。
person1.show3
是一个高阶函数,它返回了一个函数,分步走的话,应该是这样:
var func = person3.show()
func()
从而导致最终调用函数的执行环境是 window,但并不是 window 对象调用了它。所以说,this 总是指向调用该函数的对象,这句话还得补充一句:在全局函数中,this 等于 window。
person1.show3().call(person2)
与 person1.show3.call(person2)()
也好理解了。前者是通过person2调用了最终的打印方法。后者是先通过person2调用了person1的高阶函数,然后再在全局环境中执行了该打印方法。
person1.show4()()
,person1.show4().call(person2)
都是打印person1。这好像又印证了那句:箭头函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。因为即使我用过person2去调用这个箭头函数,它指向的还是person1。
然而person1.show4.call(person2)()
的结果又是 person2。this 值又发生改变,看来上述那句描述又走不通了。一步步来分析,先通过 person2 执行了 show4 方法,此时 show4 第一层函数的 this 指向的是 person2。所以箭头函数输出了 person2 的 name。也就是说,箭头函数的 this 指向的是谁调用箭头函数的外层 function,箭头函数的 this 就是指向该对象,如果箭头函数没有外层函数,则指向 window。这样去理解 show2 方法,也解释的通。
有了题目1的练手,接下来再来看看题目2的答案:
personA.show1() // personA
personA.show1.call(personB) // personB
personA.show2() // personA
personA.show2.call(personB) // personA
personA.show3()() // window
personA.show3().call(personB) // personB
personA.show3.call(personB)() // window
personA.show4()() // personA
personA.show4().call(personB) // personA
personA.show4.call(personB)() // personB
这次大家都做对了吗?
题目2跟题目1的区别是使用了构造函数去创建对象,而不是直接用字面量的方式。引用红宝书上的一段话:
使用 new 操作符调用构造函数,实际上会经历一下4个步骤:
- 创建一个对象;
- 将构造函数的作用域赋给新对象(因此this就指向了这个新对象);
- 执行构造函数中的代码(为这个新对象添加属性);
- 返回新对象。
因此构造函数的 this 是指向构造函数本身,记住这一点再按照题目1的分析思路去分析,就能得到题目2的答案啦。
浅谈HTTP缓存
什么是缓存
缓存是指存储指定资源的一份拷贝,并在下次请求该资源时提供该拷贝的技术,当 web 缓存发现请求的资源已经被存储,它会拦截请求,返回该资源的拷贝,而不会去源服务器重新下载。
为什么需要缓存
首先减少带宽
使用缓存就不需要再去下载资源,这样使得网络带宽大幅减少。
其次缓解服务器压力
服务器不需要给每次请求都返回数据,这样可以降低服务器的压力。
最重要的是提升网站的性能
使用缓存可以减少用户等待的时间,这样可以使得网站的性能上升,给用户更好的体验。
http 1.0 时代
主要通过 Pragma 和 Expires 这两个字段来规范。其实这样控制缓存很古老,但是有些较旧的客户端可能不支持 http 1.1,因此这两个头还在使用中。
Pragma
在响应头头里面设置这个字段值是 no-cache 的时候,意味着浏览器不会缓存资源,每次都要向服务器发送一次请求。
Expires
Expires 可以用来设置缓存的时间,它的值是一个格林尼治时间,比如Mon, 20 Jul 2017 10:08:35 GMT 来告诉浏览器资源缓存过期时间,如果还没过该时间点则不发请求。
当 Pragma 头部和 Expires 头部同时存在的时候 ,起作用的是 Pragma,因此当设置 Pragma 为 no-cache 之后又设置 Expires 的话还是会向服务器发送请求。
使用这两个头部控制缓存当然会存在一些问题, Expires 定义的缓存时间是相对服务器上的时间而言的,如果客户端上的时间跟服务器上的时间不一致,那就没什么用了。因为这些问题 ,后来就有了 http 1.1 时代的缓存控制。
http 1.1 时代
主要是通过 Cache-Control 来区分对缓存机制的支持,其中请求头和响应头都支持。
Cache-Control
若报文中同时出现了 Expires 和 Cache-Control,则以 Cache-Control 为准。如果同时出现了三者,优先级从高到低分别是 Pragma -> Cache-Control -> Expires 。
这个字段的值可以进行一些组合
完全不缓存
缓存中不得存储任何关于客户端请求和服务端响应的内容。每次由客户端发起的请求都会下载完整的响应内容。
Cache-Control: no-cache, no-store, must-revalidate
缓存过期
判断缓存是否过期的一个最常使用的标志是 max-age。相对 Expires 而言,max-age 是距离请求发起的时间的秒数。针对应用中那些不会改变的文件,通常可以手动设置一定的时长以保证缓存有效,例如图片、css、js等静态资源。
Cache-Control: max-age = 3600
这里指缓存有效时间是一个小时,一个小时内不发请求,但是一个小时之后需要从服务器拿到资源。
讲到这里,决定客户端发送是否发送请求的头部控制就已经说完了。但是现在还有一个同样很重要的问题,是不是客户端发送请求之后,服务器端就必须要将全部的资源发回去。如果客户端发请求之后,服务器端资源并没有改变呢?就这个问题,就需要一个缓存校验字段。
Last-Modified
服务器将资源传递给客户端时,会将资源最后更改的时间以“Last-Modified: GMT”的形式加在实体首部上一起返回给客户端。例如
Last-Modified: Fri, 23 Jul 2017 01:47:00 GMT
这个时候客户端在请求资源的时候需要发送一个字段来标记时间,服务器端会根据这个时间与服务器上该资源的最终修改时间对比,若一致,则说明该资源没有被修改过。
客户端发送的这个字段一般有两种:
If-Modified-Since: Last-Modified-value
该字段告诉服务器如果客户端传来的最后修改时间和服务器上的一致,直接返回 304 状态码即可,当前各大浏览器均是使用该字段来向服务器传递保存的 Last-Modified 的值。如果时间不一致的话,服务器会返回所有的这个资源并返回 200 状态码。
If-Unmodified-Since: Last-Modified-value
该字段告诉服务器如果客户端传来的最后修改时间和服务器上的不一致,则直接返回 412 状态码(Precondition Failed)。
ETag
使用 Last-Modified 也存在一些问题,比如:
1、一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新GET;
2、某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),If-Modified-Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒);
3、某些服务器不能精确的得到文件的最后修改时间。
为解决上面这些问题,又有 ETag 字段来标注资源的唯一性。
服务器端在返回的实体里面定义一个字段 ETag,这个字段的值是由某种算法计算出来的唯一标志符(比如md5)。
而客户端需要在请求资源的时候发送一个字段去把服务器给它的这个 ETag 值再传给服务器,服务器会根据这个值和自己这个资源的ETag值进行对比,如果不同的话就返回这个资源并返回 200, 如果相同的话只需要返回 304 告诉浏览器直接从缓存中读取就可以了。
客户端发送的这个字段一般有两种:
If-None-Match: ETag-value
告诉服务端如果 ETag 没匹配上需要重发资源数据,否则直接回送304 和响应报头即可。
当前各浏览器均是使用的该请求首部来向服务器传递保存的 ETag 值。
If-Match: ETag-value
该字段告诉服务器如果客户端传来的 Etag 值跟服务器上的不一致,则直接返回 412 状态码(Precondition Failed)。
Last-Modified 与 ETag 是可以一起使用的,服务器会优先验证 ETag,一致的情况下,才会继续比对 Last-Modified,最后才决定是否返回 304。
浏览器到底怎么判断缓存
说了这么多,这么多字段混在一起的话,浏览器究竟怎么去判断缓存?
强缓存
强缓存就是之前扯的那一大堆 Pragma Expires 以及 Cache-control,命中强缓存之后的状态码是 200,后面会跟上 from cache。当然,现在高版本的 chrome 换成了 from disk cache(磁盘缓存) 和 from memory cache(内存缓存)。
协商缓存
协商缓存就是之前扯的 Last-Modified/If-Modified-Since 或者 ETag/If-None-Match,命中协商缓存之后的状态码是 304
判别规则
1.浏览器首先根据这个资源的头部判断是否命中强缓存,如果命中就直接从缓存中加载资源,不会再发请求。
2.如果没有命中强缓存,就会发一个请求到服务器端,服务器根据这个头部里面的 If-Modified-Since或者 If-None-Match 来判断是否命中协商缓存,如果命中就返回 304,浏览器会直接从缓存中拿资源。
3.如果协商缓存也没有命中,就会直返回新的资源,并返回状态码 200。
用户行为
用户在使用地址栏回车、页面链接跳转、新开窗口以及前进后退的时候 Cache-Control/Expires 都还接着有效。
当用户在按F5进行刷新的时候,会忽略 Cache-Control/Expires 的设置,再次发送请求去服务器请求,而 Last-Modified/Etag 还是有效的,服务器会根据情况判断返回304还是200。
当用户使用Ctrl+F5进行强制刷新的时候,只是所有的缓存机制都将失效,重新从服务器拉去资源。
实际应用
当在实际应用中时,对于所有可缓存资源,一般需要指定一个 Expires 或 Cache-Control max-age 以及一个 Last-Modified 或 ETag。这样既能控制强缓存,也能控制协商缓存。但是由于 Cache-Control 只支持 http 1.1,如果需要兼容老设备,还是需要 Expires,它既支持 http1.0 也支持 http 1.1。
但是如果对于一个不是经常改变的静态资源来说,我们可以通过服务器来告诉浏览器是否需要重新请求,这样就避免了很多 304。
比如我们可以通过在 url 后面加上 md5 参数或者将 md5 参数写成文件名的一部分来实现。
当资源没有变动的时候直接使用缓存,不用发起请求,当资源发生变化时,其 url 就会发生变化。网址一经更改,系统就会强制浏览器重新抓取资源。
CSP内容安全策略使用
CSP内容安全策略使用
CSP使用方式有两种,一是使用meta标签,二是在HTTP响应头里指定Content-Security-Policy。本文探索在页面meta标签添加CSP的使用。
1. csp 基础
- 工作原理:通过一系列指令告诉客户端(如浏览器)被保护资源(如页面)内只允许加载和执行指令集中限定的内容,类似白名单机制,不满足限定条件的资源和内容将被客户端阻断或不被执行。
- 目标定位:有效减少内容注入漏洞发生后所造成的危害(而非防止内容注入漏洞的发生)
- 限制范围:当前页面本身内容以及加载到当前页面的任何内容(包括页面加载完后通过js异步加载的内容)
2. CSP规则内容
2.1 CSP Level 1 规则
指令 | 示例 | 解释 |
---|---|---|
default-src | 'self' js.example.com | 所有资源的默认加载源限制(JavaScript, Images, CSS, Font's, AJAX requests, Frames, HTML5) |
script-src | 'self' js.example.com | JS的加载源限制,会覆盖default-src中的策略。比如写了default-src xx.com;则script-src x.com xx.com;必须同时加上xx.com,因为script-src会当作一个整体覆盖整个默认的default-src规则。 |
style-src | 'self' css.example.com | CSS的加载源限制 |
img-src | 'self' *.photo.com | 图片的加载源限制 |
connect-src | 'self' *.tingyun.com | HTTP 连接(通过 XHR、WebSockets、EventSource等)的资源限制 |
font-src | 'self' at.alicdn.com | 字体文件的加载源限制 |
object-src | 'self' x.example.com | 针对object, embed, applet等flash插件的加载策略 |
media-src | media.example.com | audio, video标签中,音频视频资源限制 |
report-uri | /some-report-uri | 将由于不符合CSP而没有加载的资源上报到该url。也可以在HTTP的响应头的Content-Security-Policy后面加-Report-Only,只上报规则匹配情况,而不阻止任何资源加载 |
sandbox | allow-forms allow-scripts | 对请求的资源启用sandbox,类似于iframe的sandbox |
2.2 CSP Level 1 兼容性
CSP Level 1 标准,目前的支持程度很高,特别是移动端。而对于不支持CSP的浏览器,只是多了一些浏览器不能识别的信息,不会有什么影响,可以放心大胆地利用起来。
2.3 CSP Level 2 规则
指令 | 示例 | 解释 |
---|---|---|
child-src | 'self' | 限制子窗口(iframe、弹窗)的源,取代frame-src |
form-action | 'self' | 限制表单能够提交到的源 |
frame-ancestors | 'none' | 限制了当前页面可以被哪些页面以iframe, frame, object等方式加载 |
plugin-types | application/pdf | 限制插件的类型,例如pdf |
2.4 CSP Level 2 兼容性
CSP Level 2 标准,目前移动端IOS系统支持程度很高。android系统必须5以上才支持。
3. CSP 默认行为
在页面头部加了CSP的meta标签后,如果不做任何配置,CSP会有一些规则默认开启。
3.1 阻止内联代码执行
CSP默认配置下会阻止内联代码执行。
1.script标签
<script>
alert("Hello CSP");
</script>
2.内联事件
<a href="onclick="handleClick()"></a>
<a href="javascript:handleClick()"></a>
3.内联样式
<div style="display: none"></div>
在这种情况下,如果要允许内联代码的话,需要配置
<meta http-equiv="Content-Security-Policy" content="script-src 'unsafe-inline';style-src 'unsafe-inline';">
3.2 eval相关功能被禁用
用户输入字符串,然后经过eval()等函数转义进而被当作脚本去执行。这样的攻击方式比较常见。在CSP默认配置下,eval() , new Function() , setTimeout([string], …) 和setInterval([string], …)都被禁止运行。
如果想要开启这些功能,可以使用 'unsafe-eval' 值
<meta http-equiv="Content-Security-Policy" content="script-src 'unsafe-eval';">
4. CSP 上报日志
必须使用 report-uri 指令,并至少提供一个接收地址
Content-Security-Policy: report-uri url
上报内容为一个json对象,包含以下的数据:
{
blocked-uri : 被阻止的违规资源
document-uri : 拦截违规行为发生的页面
original-policy : Content-Security-Policy头策略的所有内容
referrer : 页面的referrer
status-code : HTTP响应状态
violated-directive : 违规的指令
}
5. CSP在网站的使用测试
1.M站使用CSP,目的是为了解决js劫持报错问题。因此CSP的规则以限制js源为主。加'unsafe-eval'是允许eval相关功能。
<meta http-equiv="Content-Security-Policy" content="script-src 'self' *.example.com 'unsafe-eval';">
2.加了CSP规则后,在测试页面引入一个不合法的外链代码。页面成功阻止了该外链代码的加载,页面有相应报错,但不影响页面正常功能。
3.在页面js代码中,测试动态创建script标签向页面植入代码,同样被禁止加载。
4.在页面html代码中加内联js代码,代码没有被执行,且控制台会报相应错误信息。
5.在移动端网页头部加入了计算rem的内联js代码,如果直接在页面头部加入csp规则,会导致这部分代码无法执行。经测试,在计算rem的内联代码之后引入csp规则的meta标签,不会禁止之前的内联js代码执行。
6.测试小结:CSP功能能满足解决目前网站被大量劫持的痛点。
6. 参考资料
exports、module.exports 和 export、export default 关系梳理
平时写代码时看到各种require、import引入模块,还有 ES6 各种 export 、export default 导出模块,有时候还能看到 module.exports 的写法导出模块,不清楚的同学可能就被搞晕了。下面就来理一理他们的关系。
使用范围:
require: node 和 es6 都支持的引入
export / import : 只有 es6 支持的导出引入
module.exports / exports: 只有 node 支持的导出(使用webpack也可以让javascript支持CommonJS规范)
node模块
Node里面的模块系统遵循的是CommonJS规范。
那么什么是CommonJS规范呢?
由于js以前比较混乱,各写各的代码,没有一个模块的概念,而这个规范出来其实就是对模块的一个定义。
CommonJS定义的模块分为: 模块标识(module)、模块定义(exports) 、模块引用(require)
先解释 exports 和 module.exports
在一个node执行一个文件时,会给这个文件内生成一个 exports和module对象,而module又有一个exports属性。他们之间的关系如下图,都指向一块{}内存区域。
exports = module.exports = {};
通过代码看看两者区别:
//utils.js
let a = 100;
console.log(module.exports); //能打印出结果为:{}
console.log(exports); //能打印出结果为:{}
exports.a = 200; //这里辛苦劳作帮 module.exports 的内容给改成 {a : 200}
exports = '指向其他内存区'; //这里把exports的指向指走
//test.js
var a = require('/utils');
console.log(a) // 打印为 {a : 200}
从上面可以看出,其实require导出的内容是module.exports的指向的内存块内容,并不是exports的。
简而言之,区分他们之间的区别就是 exports 只是 module.exports的引用,辅助后者添加内容用的。
所以,exports只辅助module.exports操作内存中的数据,最后真正被require出去的内容还是module.exports的。
为了避免出现混淆,尽量都用 module.exports 导出,然后用 require 导入。
ES中的模块导出导入
在es中的模块就非常清晰了。不过也有一些细节的东西需要搞清楚。
比如 export 和 export default,还有导入的时候,import a from ..、 import {a} from ..,总之也有点乱,那么下面我们就开始把它们搞清楚吧。
export 和 export default
首先讲讲这两个导出的区别
- export 与 export default 均可用于导出常量、函数、文件、模块等;
- 在一个文件或模块中,export、import 可以有多个,export default 仅有一个;
- 通过 export 方式导出,在导入时要加{ },export default则不需要;
- export 能直接导出变量表达式,export default 不行。
通过代码看看两者区别:
- testEs6Export.js
'use strict'
//导出变量
export const a = '100';
//导出方法
export const dogSay = function(){
console.log('wang wang');
}
//导出方法第二种
function catSay(){
console.log('miao miao');
}
export { catSay };
//export default导出
const m = 100;
export default m;
//export defult const m = 100;// 这里不能写这种格式。
- index.js
//index.js
'use strict'
var express = require('express');
var router = express.Router();
import { dogSay, catSay } from './testEs6Export'; //导出了 export 方法
import m from './testEs6Export'; //导出了 export default
import * as testModule from './testEs6Export';//as 集合成对象导出
/* GET home page. */
router.get('/', function(req, res, next) {
dogSay();
catSay();
console.log(m);
testModule.dogSay();
console.log(testModule.m); // undefined , 因为 as 导出是 把 零散的 export 聚集在一起作为一个对象,而export default 是导出为 default属性。
console.log(testModule.default); // 100
res.send('恭喜你,成功验证');
});
module.exports = router;
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.