//源码
for(let i=0; i<10; i++){
console.log(i)
}
console.log(i)
babel转码
for(var _i = 0; _i < 10; _i++){
console.log(_i)
}
console.log(i)
(function(){
for(var i = 0; i < 5; i ++){
console.log(i) // 0 1 2 3 4
}
})();
console.log(i) // Uncaught ReferenceError: i is not defined
实现const的关键在于Object.defineProperty()这个API,这个API用于在一个对象上增加或修改属性。通过配置属性描述符,可以精确地控制属性行为。Object.defineProperty() 接收三个参数:
function _const(key, value) {
const desc = {
value,
writable: false
}
Object.defineProperty(window, key, desc)
}
_const('obj', {
a: 1}) //定义obj
obj.b = 2 //可以正常给obj的属性赋值
obj = {
} //抛出错误,提示对象read-only
call()的原理比较简单,由于函数的this指向它的直接调用者,我们变更调用者即完成this指向的变更:
//变更函数调用者示例
function foo() {
console.log(this.name)
}
// 测试
const obj = {
name: '写代码像蔡徐抻'
}
obj.foo = foo // 变更foo的调用者
obj.foo() // '写代码像蔡徐抻'
Function.prototype.myCall = function(thisArg, ...args) {
thisArg.fn = this // this指向调用call的对象,即我们要改变this指向的函数
return thisArg.fn(...args) // 执行函数并return其执行结果
}
Function.prototype.myCall = function(thisArg, ...args) {
const fn = Symbol('fn') // 声明一个独有的Symbol属性, 防止fn覆盖已有属性
thisArg = thisArg || window // 若没有传入this, 默认绑定window对象
thisArg[fn] = this // this指向调用call的对象,即我们要改变this指向的函数
const result = thisArg[fn](...args) // 执行当前函数
delete thisArg[fn] // 删除我们声明的fn属性
return result // 返回函数执行结果
}
//测试
foo.myCall(obj) // 输出'写代码像蔡徐抻'
Function.prototype.myApply = function(thisArg, args) {
const fn = Symbol('fn') // 声明一个独有的Symbol属性, 防止fn覆盖已有属性
thisArg = thisArg || window // 若没有传入this, 默认绑定window对象
thisArg[fn] = this // this指向调用call的对象,即我们要改变this指向的函数
const result = thisArg[fn](...args) // 执行当前函数
delete thisArg[fn] // 删除我们声明的fn属性
return result // 返回函数执行结果
}
//测试
foo.myApply(obj, []) // 输出'写代码像蔡徐抻'
从用法上看,似乎给call/apply包一层function就实现了bind():
Function.prototype.myBind = function(thisArg, ...args) {
return () => {
this.apply(thisArg, args)
}
}
Function.prototype.myBind = function (thisArg, ...args) {
var self = this
// new优先级
var fbound = function () {
self.apply(this instanceof self ? this : thisArg, args.concat(Array.prototype.slice.call(arguments)))
}
// 继承原型上的属性和方法
fbound.prototype = Object.create(self.prototype);
return fbound;
}
//测试
const obj = {
name: '写代码像蔡徐抻' }
function foo() {
console.log(this.name)
console.log(arguments)
}
foo.myBind(obj, 'a', 'b', 'c')() //输出写代码像蔡徐抻 ['a', 'b', 'c']
function debounce(func, wait) {
let timeout = null
return function() {
let context = this
let args = arguments
if (timeout) clearTimeout(timeout)
timeout = setTimeout(() => {
func.apply(context, args)
}, wait)
}
}
方式一 延时器
function throttle(func, wait) {
let timeout = null
return function() {
let context = this
let args = arguments
if (!timeout) {
timeout = setTimeout(() => {
timeout = null
func.apply(context, args)
}, wait)
}
}
}
实现方式2:使用两个时间戳prev旧时间戳和now新时间戳,每次触发事件都判断二者的时间差,如果到达规定时间,执行函数并重置旧时间戳
function throttle(func, wait) {
var prev = 0;
return function() {
let now = Date.now();
let context = this;
let args = arguments;
if (now - prev > wait) {
func.apply(context, args);
prev = now;
}
}
}
const arr = [1, [1,2], [1,2,3]]
arr.flat(Infinity) // [1, 1, 2, 1, 2, 3]
const arr = [1, [1,2], [1,2,3]]
const str = `[${
JSON.stringify(arr).replace(/(\[|\])/g, '')}]`
JSON.parse(str) // [1, 1, 2, 1, 2, 3]
const arr = [1, [1,2], [1,2,3]]
function flat(arr) {
let result = []
for (const item of arr) {
item instanceof Array ? result = result.concat(flat(item)) : result.push(item)
}
return result
}
flat(arr) // [1, 1, 2, 1, 2, 3]
const arr = [1, [1,2], [1,2,3]]
function flat(arr) {
return arr.reduce((prev, cur) => {
return prev.concat(cur instanceof Array ? flat(cur) : cur)
}, [])
}
flat(arr) // [1, 1, 2, 1, 2, 3]
let arr = [1, [1,2], [1,2,3]]
while (arr.some(Array.isArray)) {
arr = [].concat(...arr);
}
console.log(arr) // [1, 1, 2, 1, 2, 3]
异步编程二三事 | Promise/async/Generator实现原理解析 | 9k字
在JS中一切皆对象,但JS并不是一种真正的面向对象(OOP)的语言,因为它缺少类(class)的概念。虽然ES6引入了class和extends,使我们能够轻易地实现类和继承。但JS并不存在真实的类,JS的类是通过函数以及原型链机制模拟的,本小节的就来探究如何在ES5环境下利用函数和原型链实现JS面向对象的特性。
在开始之前,我们先回顾一下原型链的知识,后续new和继承等实现都是基于原型链机制。很多介绍原型链的资料都能写上洋洋洒洒几千字,但我觉得读者们不需要把原型链想太复杂,容易把自己绕进去,其实在我看来,原型链的核心只需要记住三点:
// new是关键字,这里我们用函数来模拟,new Foo(args) <=> myNew(Foo, args)
function myNew(foo, ...args) {
// 创建新对象,并继承构造方法的prototype属性, 这一步是为了把obj挂原型链上, 相当于obj.__proto__ = Foo.prototype
let obj = Object.create(foo.prototype)
// 执行构造方法, 并为其绑定新this, 这一步是为了让构造方法能进行this.name = name之类的操作, args是构造方法的入参, 因为这里用myNew模拟, 所以入参从myNew传入
let result = foo.apply(obj, args)
// 如果构造方法已经return了一个对象, 那么就返回该对象, 一般情况下,构造方法不会返回新实例,但使用者可以选择返回新实例来覆盖new创建的对象 否则返回myNew创建的新对象
return typeof result === 'object' && result !== null ? result : obj
}
function Foo(name) {
this.name = name
}
const newObj = myNew(Foo, 'zhangsan')
console.log(newObj) // Foo {name: "zhangsan"}
console.log(newObj instanceof Foo) // true
// 父类
function Parent() {
this.name = '写代码像蔡徐抻'
}
// 父类的原型方法
Parent.prototype.getName = function() {
return this.name
}
// 子类
function Child() {
}
// 让子类的原型对象指向父类实例, 这样一来在Child实例中找不到的属性和方法就会到原型对象(父类实例)上寻找
Child.prototype = new Parent()
Child.prototype.constructor = Child // 根据原型链的规则,顺便绑定一下constructor, 这一步不影响继承, 只是在用到constructor时会需要
// 然后Child实例就能访问到父类及其原型上的name属性和getName()方法
const child = new Child()
child.name // '写代码像蔡徐抻'
child.getName() // '写代码像蔡徐抻'
原型继承的缺点:
// 示例:
function Parent() {
this.name = ['写代码像蔡徐抻']
}
Parent.prototype.getName = function() {
return this.name
}
function Child() {
}
Child.prototype = new Parent()
Child.prototype.constructor = Child
// 测试
const child1 = new Child()
const child2 = new Child()
child1.name[0] = 'foo'
console.log(child1.name) // ['foo']
console.log(child2.name) // ['foo'] (预期是['写代码像蔡徐抻'], 对child1.name的修改引起了所有child实例的变化
function Parent(name) {
this.name = [name]
}
Parent.prototype.getName = function() {
return this.name
}
function Child() {
Parent.call(this, 'zhangsan') // 执行父类构造方法并绑定子类的this, 使得父类中的属性能够赋到子类的this上
}
//测试
const child1 = new Child()
const child2 = new Child()
child1.name[0] = 'foo'
console.log(child1.name) // ['foo']
console.log(child2.name) // ['zhangsan']
child2.getName() // 报错,找不到getName(), 构造函数继承的方式继承不到父类原型上的属性和方法
构造函数继承的缺点:
既然原型链继承和构造函数继承各有互补的优缺点, 那么我们为什么不组合起来使用呢, 所以就有了综合二者的组合式继承
function Parent(name) {
this.name = [name]
}
Parent.prototype.getName = function() {
return this.name
}
function Child() {
// 构造函数继承
Parent.call(this, 'zhangsan')
}
//原型链继承
Child.prototype = new Parent()
Child.prototype.constructor = Child
//测试
const child1 = new Child()
const child2 = new Child()
child1.name[0] = 'foo'
console.log(child1.name) // ['foo']
console.log(child2.name) // ['zhangsan']
child2.getName() // ['zhangsan']
每次创建子类实例都执行了两次构造函数(Parent.call()和new Parent()),虽然这并不影响对父类的继承,但子类创建实例时,原型中会存在两份相同的属性和方法,这并不优雅
4)、寄生式组合继承
为了解决构造函数被执行两次的问题, 我们将指向父类实例改为指向父类原型, 减去一次构造函数的执行
function Parent(name) {
this.name = [name]
}
Parent.prototype.getName = function() {
return this.name
}
function Child() {
// 构造函数继承
Parent.call(this, 'zhangsan')
}
//原型链继承
// Child.prototype = new Parent()
Child.prototype = Parent.prototype //将`指向父类实例`改为`指向父类原型`
Child.prototype.constructor = Child
//测试
const child1 = new Child()
const child2 = new Child()
child1.name[0] = 'foo'
console.log(child1.name) // ['foo']
console.log(child2.name) // ['zhangsan']
child2.getName() // ['zhangsan']
但这种方式存在一个问题,由于子类原型和父类原型指向同一个对象,我们对子类原型的操作会影响到父类原型,例如给Child.prototype增加一个getName()方法,那么会导致Parent.prototype也增加或被覆盖一个getName()方法,为了解决这个问题,我们给Parent.prototype做一个浅拷贝
function Parent(name) {
this.name = [name]
}
Parent.prototype.getName = function() {
return this.name
}
function Child() {
// 构造函数继承
Parent.call(this, 'zhangsan')
}
//原型链继承
// Child.prototype = new Parent()
Child.prototype = Object.create(Parent.prototype) //将`指向父类实例`改为`指向父类原型`
Child.prototype.constructor = Child
//测试
const child = new Child()
const parent = new Parent()
child.getName() // ['zhangsan']
parent.getName() // 报错, 找不到getName()
到这里我们就完成了ES5环境下的继承的实现,这种继承方式称为寄生组合式继承,是目前最成熟的继承方式,babel对ES6继承的转化也是使用了寄生组合式继承
一开始最容易想到的是原型链继承,通过把子类实例的原型指向父类实例来继承父类的属性和方法,但原型链继承的缺陷在于对子类实例继承的引用类型的修改会影响到所有的实例对象以及无法向父类的构造方法传参。
因此我们引入了构造函数继承, 通过在子类构造函数中调用父类构造函数并传入子类this来获取父类的属性和方法,但构造函数继承也存在缺陷,构造函数继承不能继承到父类原型链上的属性和方法。
所以我们综合了两种继承的优点,提出了组合式继承,但组合式继承也引入了新的问题,它每次创建子类实例都执行了两次父类构造方法,我们通过将子类原型指向父类实例改为子类原型指向父类原型的浅拷贝来解决这一问题,也就是最终实现 —— 寄生组合式继承。
JS引擎中对变量的存储主要有两种位置,栈内存和堆内存,栈内存存储基本类型数据以及引用类型数据的内存地址,堆内存储存引用类型的数据
栈内存的回收:
DOM样式发生了变化,但没有影响DOM的几何属性时,会触发重绘,而不会触发回流。重绘由于DOM位置信息不需要更新,省去了布局过程,因而性能上优于回流
1 Service Worker
2 Memory Cache(内存缓存)
3 Disk Cache(硬盘缓存)
4 Push Cache(推送缓存)
5 以上缓存都没命中就会进行网络请求
即过期时间,例如「Expires: Thu, 26 Dec 2019 10:30:42 GMT」表示缓存会在这个时间后失效,这个过期日期是绝对日期,如果修改了本地日期,或者本地日期与服务器日期不一致,那么将导致缓存过期时间错误。
HTTP/1.1新增字段,Cache-Control可以通过max-age字段来设置过期时间,例如「Cache-Control:max-age=3600」除此之外Cache-Control还能设置private/no-cache等多种字段
即最后修改时间,浏览器第一次请求资源时,服务器会在响应头上加上Last-Modified ,当浏览器再次请求该资源时,浏览器会在请求头中带上If-Modified-Since 字段,字段的值就是之前服务器返回的最后修改时间,服务器对比这两个时间,若相同则返回304,否则返回新资源,并更新Last-Modified
HTTP/1.1新增字段,表示文件唯一标识,只要文件内容改动,ETag就会重新计算。缓存流程和 Last-Modified 一样:服务器发送 ETag 字段 -> 浏览器再次请求时发送 If-None-Match -> 如果ETag值不匹配,说明文件已经改变,返回新资源并更新ETag,若匹配则返回304
标准答案:
GET请求参数放在URL上,POST请求参数放在请求体里
GET请求参数长度有限制,POST请求参数长度可以非常大
POST请求相较于GET请求安全一点点,因为GET请求的参数在URL上,且有历史记录
GET请求能缓存,POST不能
更进一步:
其实HTTP协议并没有要求GET/POST请求参数必须放在URL上或请求体里,也没有规定GET请求的长度,目前对URL的长度限制,是各家浏览器设置的限制。GET和POST的根本区别在于:GET请求是幂等性的,而POST请求不是
幂等性,指的是对某一资源进行一次或多次请求都具有相同的副作用。例如搜索就是一个幂等的操作,而删除、新增则不是一个幂等操作。
由于GET请求是幂等的,在网络不好的环境中,GET请求可能会重复尝试,造成重复操作数据的风险,因此,GET请求用于无副作用的操作(如搜索),新增/删除等操作适合用POST
一个HTTP请求报文由请求行(request line)、请求头(header)、空行和请求数据4个部分组成
响应报文和请求报文结构类似,不再赘述