前端复习企划3-JS与ES6
JS
基本数据类型与类型判断
基本数据类型
6种,boolean,number,string,undefined,null,symbol
null是Object吗
虽然 typeof null会输出 object,但是这只是 JS 存在的一个悠久 Bug。在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000
开头代表是对象,然而 null
表示为全零,所以将它错误的判断为 object
。虽然现在的内部类型判断代码已经改变了,但是对于这个 Bug 却是一直流传下来。
原始类型与对象类型区别
对象类型和原始类型不同的是,原始类型存储的是值,对象类型存储的是地址。
当创建了一个对象类型的时候,计算机会在内存中帮我们开辟一个空间来存放值,但是我们需要找到这个空间,这个空间会拥有一个地址(指针)。
typeof vs instanceof
typeof对于原始类型来说,除null会显示成object,其余都可以显示正确的类型typeof 对于对象来说,除了函数都会显示 object,所以说typeof并不能准确判断变量到底是什么类型。
如果我们想判断一个对象的正确类型,这时候可以考虑使用 instanceof,因为内部机制是通过原型链来判断的。
如何判断一个对象类型是数组
- 根据构造函数来判断 xxx instanceof Array
- 根据class属性判断 Object.prototype.toString.call(obj)==='[object Array]'
- 直接用isArray判断
类型转换
在 JS 中类型转换只有三种情况,分别是:
- 转换为布尔值
- 转换为数字
- 转换为字符串
转Boolean
在条件判断时,除了 undefined
, null
, false
, NaN
, ''
, 0
, -0
,其他所有值都转为 true
,包括所有对象。
对象转原始类型
对象在转换类型的时候,会调用内置的 [[ToPrimitive]]
函数,对于该函数来说,算法逻辑一般来说如下:
- 如果已经是原始类型了,那就不需要转换了
- 如果需要转字符串类型就调用
x.toString()
,转换为基础类型的话就返回转换的值。不是字符串类型的话就先调用valueOf
,结果不是基础类型的话再调用toString
- 调用
x.valueOf()
,如果转换为基础类型,就返回转换的值 - 如果都没有返回原始类型,就会报错
当然可以重写 Symbol.toPrimitive
,该方法在转原始类型时调用优先级最高。
let a = { valueOf() { return 0 }, toString() { return '1' }, [Symbol.toPrimitive]() { return 2 } } 1 + a // => 3
四则运算符
加法运算符不同于其他几个运算符,它有以下几个特点:
- 运算中其中一方为字符串,那么就会把另一方也转换为字符串
- 如果一方不是字符串或者数字,那么会将它转换为数字或者字符串
1 + '1' // '11' true + true // 2 4 + [1,2,3] // "41,2,3"
如果你对于答案有疑问的话,请看解析:
- 对于第一行代码来说,触发特点一,所以将数字
1
转换为字符串,得到结果'11'
- 对于第二行代码来说,触发特点二,所以将
true
转为数字1
- 对于第三行代码来说,触发特点二,所以将数组通过
toString
转为字符串1,2,3
,得到结果41,2,3
另外对于加法还需要注意这个表达式 'a' + + 'b'
'a' + + 'b' // -> "aNaN"
因为 + 'b'
等于 NaN
,所以结果为 "aNaN"
。
对于除了加法的运算符来说,只要其中一方是数字,那么另一方就会被转为数字
4 * '3' // 12 4 * [] // 0 4 * [1, 2] // NaN
比较运算符
- 如果是对象,就通过
toPrimitive
转换对象 - 如果是字符串,就通过
unicode
字符索引来比较
let a = { valueOf() { return 0 }, toString() { return '1' } } a > -1 // true
在以上代码中,因为 a
是对象,所以会通过 valueOf
转换为原始类型再比较值。
==比较
- Boolean,number,string三类比较的时候把值转换成数字,在看转换结果是否相等。证明:('1'==true) 是真 ('abc'==true)是假。
- undefined 参与比较,换成了NaN,所以其他三个类型跟它比较都是false,跟null类型比较的时候是true。(NaN==NaN)是假
- null参与比较,被当成对象,因为null没有valueof和toString,除了undefined谁跟他比较都是false。
- 值类型与对象比较:先调用对象valueof 如果仍返回对象,调用tostring,如果还是没有就不等。
==与===
对于 ==
来说,如果对比双方的类型不一样的话,就会进行类型转换。
假如我们需要对比 x
和 y
是否相同,就会进行如下判断流程:
首先会判断两者类型是否相同。相同的话就是比大小了
类型不相同的话,那么就会进行类型转换
会先判断是否在对比
null
和undefined
,是的话就会返回true
判断两者类型是否是string和number,是的话就会将字符串转换为number
1 == '1' ↓ 1 == 1
判断其中一方是否为boolean,是的话就会把boolean转为number再进行判断
'1' == true ↓ '1' == 1 ↓ 1 == 1
判断其中一方是否object且另一方为string、number或者symbol,是的话就会把object转为原始类型再进行判断
'1' == { name: 'xxx' } ↓ '1' == '[object Object]'
现在来想想[]==![]输出什么?
首先先执行的是![],它会得到false。
然后[]==false,返回true。
那么[]==[],{}=={}又输出什么呢?
类型一致,它们是引用类型,地址是不一样的,所以为false!
对于 ===
来说就简单多了,就是判断两者类型和值是否相同。
面向对象
JS有两种属性值:数据属性 访问器属性
数据属性
包含一个数据值的位置
四个特性
- Configurable 表示能否通过delete删除属性从而重新定义属性,默认为true
- Enumerable 表示能否通过for-in循环返回属性,默认为true
- Writable 表示能否修改属性的值,默认为true
- Value 包含这个属性的数据值 默认为undefined
//使用字面量创建对象,前三个属性默认值为true,value为指定的值 var person={ name:'xxxx' }
修改属性默认值
- Object.defineProperty(obj,attr,{})
var person={} Object.defineProperty(person,'name',{ wirtable:false, value:'xxxx', configurable:false;//一旦被设置为false,不可以更改 }) person.name='xx' console.log(person.name);//xxxx ,严格模式下会报错 delete person.name//无效 严格模式下会报错
访问器属性
四个特性
- Configuable 表示能否通过delete删除属性从而重新定义属性,默认是true
- enumerable 表示能否通过for-in 循环返回属性,默认是true
- get 读取属性时调用函数,默认undefined
- ste 写入属性时调用函数,默认undefined
var person={ _age:20,//下划线写法表示只能通过对象方法访问的属性 state:'young' } Object.defineProperty(person,'age',{ get:function(){ return this._age; }, set:function(newVal){ if(newVal>50){ this._age=newVal; this.stae='old'; }else{ this._age=newVal; } } })
- 定义多个属性,使用Object.defineProperties();
读取属性的特性
- 使用Object.getOwnPropertyDescriptor()
var person={} Object.defineProperties(person,{ _age:{ values:20; }, state:{ value:'young'; }, age:{ get:function(){ return this._age; }, set:function(newVal){ if(newVal>50){ this._age=newVal; this.stae='old'; }else{ this._age=newVal; } } } }) var descriptor=Object.getOwnPropertyDescriptor(person,'_age'); descriptor.value//20 descriptor.configurable//false var descriptor=Object.getOwnPropertyDescriptor(person,'age'); descriptor.value//undefined descriptor.configurable//false typeof descriptor //function
深拷贝与浅拷贝
浅拷贝
Object.assign
let a = { age: 1 } let b = Object.assign({}, a) a.age = 2 console.log(b.age) // 1
- 展开运算符
...
let a = { age: 1 } let b = { ...a } a.age = 2 console.log(b.age) // 1
深拷贝
JSON.parse(JSON.stringify(object))
let a = { age: 1, jobs: { first: 'FE' } } let b = JSON.parse(JSON.stringify(a)) a.jobs.first = 'native' console.log(b.jobs.first) // FE
局限性 :会忽略 undefined
;会忽略 symbol
;不能序列化函数;不能解决循环引用的对象
- 写一个:
function deepClone(obj) { function isObject(o) { return (typeof o === 'object' || typeof o === 'function') && o !== null } if (!isObject(obj)) { throw new Error('非对象') } let isArray = Array.isArray(obj) let newObj = isArray ? [...obj] : { ...obj } Reflect.ownKeys(newObj).forEach(key => { newObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key] }) return newObj }
this
四种绑定规则
默认绑定
- 全局环境,默认绑定到window
- 函数独立调用的时候,this默认绑定到window
- 被嵌套的函数独立调用时,this默认绑定到window
- 立即执行的函数this是window
- 闭包的this默认绑定到window
隐式绑定
- 被直接对象所包含的函数调用时,也称为方法调用,this隐式绑定到该直接对象
显示绑定
- 通过call()、apply()、bind()方法把对象绑定到this上,叫做显式绑定。对于被调用的函数来说,叫做间接调用
new绑定
- 如果函数或者方法调用之前带有关键字new,它就构成构造函数调用。对于this绑定来说,称为new绑定
优先级
首先,new
的方式优先级最高,接下来是 bind
这些函数,然后是 obj.foo()
这种调用方式,最后是 foo
这种调用方式,同时,箭头函数的 this
一旦被绑定,就不会再被任何方式所改变。
实例与解析
先来看几个函数调用的场景:
function foo() { console.log(this.a) } var a = 1 foo(); const obj = { a: 2, foo: foo } obj.foo() const c = new foo()
接下来一个个分析上面几个场景
- 对于直接调用
foo
来说,不管foo
函数被放在了什么地方,this
一定是window
- 对于
obj.foo()
来说,我们只需要记住,谁调用了函数,谁就是this
,所以在这个场景下foo
函数中的this
就是obj
对象 - 对于
new
的方式来说,this
被永远绑定在了c
上面,不会被任何方式改变this
说完了以上几种情况,其实很多代码中的 this
应该就没什么问题了。
下面让我们看看箭头函数中的 this
function a() { return () => { return () => { console.log(this) } } } console.log(a()()())
首先箭头函数其实是没有 this
的,箭头函数中的 this
只取决包裹箭头函数的第一个普通函数的 this
。在这个例子中,因为包裹箭头函数的第一个普通函数是 a
,所以此时的 this
是 window
。另外对箭头函数使用 bind
这类函数是无效的。
最后种情况也就是 bind
这些改变上下文的 API 了,对于这些函数来说,this
取决于第一个参数,如果第一个参数为空,那么就是 window
。
那么说到 bind
,如果对一个函数进行多次 bind
,那么上下文会是什么呢?
let a = {} let fn = function () { console.log(this) } fn.bind().bind(a)() // => ?
如果认为输出结果是 a
,那么就错了。
其实可以把上述代码转换成另一种形式:
// fn.bind().bind(a) 等价于 let fn2 = function fn1() { return function() { return fn.apply() }.apply(a) } fn2()
可以从上述代码中发现,不管我们给函数 bind
几次,fn
中的 this
永远由第一次 bind
决定,所以结果永远是 window
。
闭包与作用域链
执行环境与作用域
- 执行环境:定义了变量或函数有权访问其他数据,决定了它们各自的行为
- 全局执行环境是window对象,即全局变量和函数都是作为window对象的属性和方法创建的,每个函数都有自己的执行环境
- 变量对象:每个执行怀静都有一个与之相关的变量对象,执行环境中定义的所有变量和函数都会保存在这个对象中
- 作用域链:代码执行过程中会创建变量对象的一个作用域链,作用域链的前端是当前执行怀静的变量对象,末端是全局执行环境的变量对象。作用域链的作用是,保证执行环境里有权访问的变量和函数是有序的,作用域链的变量只能向上访问,变量访问到window对象即被终止,作用域链向下访问不行
- 标识符的解析是沿着作用域链一级一级地搜索标识符的过程,从作用域的前端开始逐级向后回调,直到找到为止。
- 延长作用域:执行环境类型一般只有全局和局部函数两种 try-catch语句的catch块和with语句会在作用域链的前端增加一个变量对象。
闭包
- 定义:闭包就是能够读取其他函数内部变量的函数,其实就是利用了作用域链向上查找的特点。
- 作用:读取函数内部变量,让这些变量的值一直保持在内存中。
作用域链
- 作用域链针对函数作用域来说的,比如创建了一个函数,函数里面又包含了一个函数,那么就会有全局作用域、函数1的作用域、函数2的作用域。
- 查找规范:先在自己的变量范围中找,如果找不到就沿着作用域往上找。
原型链
说一下什么是原型链
在js中,每个函数都有prototype属性,这个属性值是一个对象,同时它带有constructor属性,指向这个构造函数。
通过new可以创建一个函数的实例,每个实例都有_proto_
属性,指向构造函数的prototype对象。
当我们在一个对象或者方法获取某个值的时候,会先查找实例上是否存在这个值,如果没有的话,就在原型里查找。
怎么判断一个属性是对象上的属性还是其原型对象上的属性
使用hasOwnProperty()返回true,说明是这个对象上的;如果返回false,但是属性in 这个对象返回了true,说明是原型对象上的属性。如果都是false,那么不存在这个属性。
继承
继承的原理
还是跟原型链有关。每个函数都有个原型对象,这个对象用来存储通过这个函数所创建的所有实例的共有属性和方法。在读取某个对象属性的时候,从实例开始,如果实例有就返回,如果没有就找原型对象,找到了就返回。通过实例只能访问原型对象里的值,但是不能修改。这就实现了继承。
组合继承
组合继承是最常用的继承方式,
function Parent(value) { this.val = value } Parent.prototype.getValue = function() { console.log(this.val) } function Child(value) { Parent.call(this, value) } Child.prototype = new Parent() const child = new Child(1) child.getValue() // 1 child instanceof Parent // true
以上继承的方式核心是在子类的构造函数中通过 Parent.call(this)
继承父类的属性,然后改变*子类的原型为 new Parent()
*来继承父类的函数。
这种继承方式优点在于构造函数可以传参,不会与父类引用属性共享,可以复用父类的函数,但是也存在一个缺点就是在继承父类函数的时候调用了父类构造函数,导致子类的原型上多了不需要的父类属性,存在内存上的浪费。
寄生组合继承
这种继承方式对组合继承进行了优化,组合继承缺点在于继承父类函数时调用了构造函数,我们只需要优化掉这点就行了。
function Parent(value) { this.val = value } Parent.prototype.getValue = function() { console.log(this.val) } function Child(value) { Parent.call(this, value) } Child.prototype = Object.create(Parent.prototype, { constructor: { value: Child, enumerable: false, writable: true, configurable: true } }) const child = new Child(1) child.getValue() // 1 child instanceof Parent // true
以上继承实现的核心就是将父类的原型赋值给了子类,并且将构造函数设置为子类,这样既解决了无用的父类属性问题,还能正确的找到子类的构造函数。
Class 继承
以上两种继承方式都是通过原型去解决的,在 ES6 中,我们可以使用 class
去实现继承,并且实现起来很简单
class Parent { constructor(value) { this.val = value } getValue() { console.log(this.val) } } class Child extends Parent { constructor(value) { super(value) } } let child = new Child(1) child.getValue() // 1 child instanceof Parent // true
class
实现继承的核心在于使用 extends
表明继承自哪个父类,并且在子类构造函数中必须调用 super
,因为这段代码可以看成 Parent.call(this, value)
。
Class中的箭头函数和普通函数有何区别
箭头函数相当于绑定上了this,类似构造函数内的一个值。
箭头函数会绑定在实例对象上,普通函数最后会绑定在原型上。
DOM与事件
DOM如何创建元素
创建
createHTML('div');
添加
appendChild(element)
insertBefore(insertdom.chosendom)
DOM获取元素的方式
通过元素类型获取
- document.getElementById();//id名,在实际开发中较少使用,选择器中多用class id一般只用在顶级层存在 不能太过依赖id
- document.getElementsByTagName();//标签名
- document.getElementsByClassName();//类名
- document.getElementsByName();//name属性值,一般不用
- document.querySelector();//css选择符模式,返回与该模式匹配的第一个元素,结果为一个元素;如果没找到匹配的元素,则返回null
- document.querySelectorAll()//css选择符模式,返回与该模式匹配的所有元素,结果为一个类数组
根据关系树来选择
- parentNode//获取所选节点的父节点,最顶层的节点为#document
- childNodes //获取所选节点的子节点们
- firstChild //获取所选节点的第一个子节点
- lastChild //获取所选节点的最后一个子节点
- nextSibling //获取所选节点的后一个兄弟节点 列表中最后一个节点的nextSibling属性值为null
- previousSibling //获取所选节点的前一兄弟节点 列表中第一个节点的previousSibling属性值为null
根据元素节点树来选择
- parentElement //返回当前元素的父元素节点(IE9以下不兼容)
- children // 返回当前元素的元素子节点
- firstElementChild //返回的是第一个元素子节点(IE9以下不兼容)
- lastElementChild //返回的是最后一个元素子节点(IE9以下不兼容)
- nextElementSibling //返回的是后一个兄弟元素节点(IE9以下不兼容)
- previousElementSibling //返回的是前一个兄弟元素节点(IE9以下不兼容)
如何给元素添加事件
- 在HTML元素中绑定事件 onclick=show()
- 获取dom,dom.onclick
- addEventListener(click,show,1/0)
addEventListener三个参数,取值意思
第一个参数是事件类型,第二个是事件发生的回调函数,第三个是个布尔值,默认是false,false是冒泡阶段执行,true是捕获阶段。
节点属性中children和childNodes有什么区别?
- childNodes返回的是节点的子节点集合(NodeLists),包括元素节点、文本节点还有属性节点。
- children返回的只是节点的元素节点集合(HTMLCollection)
HTMLCollection和NodeList的比较
共同点
都是类数组对象,都有length属性
都有共同的方法:item,可以通过item(index)获取返回结果的元素
都是实时变动的,document上面的更改会反映到相关的对象上
注:querySeletorAll返回的NodeList是个浅拷贝的类数组对象,在节点数目上是非实时的,不过对节点属性进行修改,还是实时反映的。
区别
- NodeList可以包含任何节点类型,HTMLCollection只包含元素节点。elementNode就是HTML中的标签。
- HTMLCollection比NodeList多一个方法:nameitem(),除了可以用id,还可以用name来获取节点信息。
事件冒泡与事件捕获
事件是先捕获,后冒泡
捕获阶段是外部元素先触发然后触发内部元素
冒泡阶段是内部元素先触发然后触发外部元素
如何阻止事件冒泡?如何取消默认事件?如何阻止事件的默认行为?
阻止事件冒泡:
W3C: stopPropagation();
IE: e.cancelBubble=true;
写法 :
window.event ? window.event.cancelBubble=true:e.stop(Propagation)
取消默认事件
W3C:preventDefault()
IE: e.returnValue:false;
阻止默认行为:
return false
原生的js会阻止默认行为,但会继续冒泡;
jquery会阻止默认行为,并停止冒泡。
获取DOM节点get系列和query系列哪种性能好?
1.从性能上说get系列的性能都比query系列好,get系列里面各有差异,这些差异可以结合算法如何遍历搜索去理解,都解释得通。
2.getElementsByTagName
比querySelectorAll
快的原因在于:getElementsByTagName
创建的过程不需要做任何操作,只需要返回一个指针即可。而querySelectorAll
会循环遍历所有的的结果,然后创建一个新的NodeList。
3.实际在用的过程中取决于要获取的是什么,再进行选择。
ES6
变量提升与var、let 及 const
首先,我们先了解提升(hoisting)这个概念:
console.log(a) // undefined var a = 1
从上述代码中我们可以发现,虽然变量还没有被声明,但是我们却可以使用这个未被声明的变量,这种情况就叫做提升,并且提升的是声明。
对于这种情况,我们可以把代码这样来看
var a console.log(a) // undefined a = 1
接下来,再来看一个例子:
var a = 10 var a console.log(a)
对于这个例子,如果你认为打印的值为 undefined
那么就错了,答案应该是 10
,对于这种情况,等价于下面这样:
var a var a a = 10 console.log(a)
其实不仅变量会提升函数也会被提升:
console.log(a) // ƒ a() {} function a() {} var a = 1
对于上述代码,打印结果会是 ƒ a() {}
,即使变量声明在函数之后,这也说明了函数会被提升,并且优先于变量提升。
使用 var
声明的变量会被提升到作用域的顶部,接下来我们再来看 let
和 const
。
还是来看一个例子:
var a = 1 let b = 1 const c = 1 console.log(window.b) // undefined console.log(window. c) // undefined function test(){ console.log(a) let a } test();//报错
首先在全局作用域下使用 let
和 const
声明变量,变量并不会被挂载到 window
上,这一点就和 var
声明有了区别。
再者当我们在声明 a
之前如果使用了 a
,就会出现报错:
Uncaught ReferenceError: Cannot access 'a' before initialization
报错的原因是因为存在暂时性死区,我们不能在声明前就使用变量,这也是 let
和 const
优于 var
的一点。然后这里你认为的提升和 var
的提升是有区别的,虽然变量在编译的环节中被告知在这块作用域中可以访问,但是访问是受限制的。
var
、let
及 const
区别其实已经不言而喻了。
其实提升存在的根本原因就是为了解决函数间互相调用的情况
function test1() { test2() } function test2() { test1() } test1();
假如不存在提升这个情况,那么就实现不了上述的代码,因为不可能存在 test1
在 test2
前面然后 test2
又在 test1
前面。
*小结: *
- 函数提升优先于变量提升,函数提升会把整个函数挪到作用域顶部,变量提升只会把声明挪到作用域顶部
var
存在提升,我们能在声明之前使用。let
、const
因为暂时性死区的原因,不能在声明前使用var
在全局作用域下声明变量会导致变量挂载在window
上,其他两者不会let
和const
作用基本一致,但是后者声明的变量不能再次赋值
let const 及var的区别
let
作用域是块级作用域(之前只有函数作用域和全局作用域)
不存在变量声明提前 (在let之前使用,会报错 is not defined)
不可以重复定义 (var可以,不会报错,但是let 会说has already been declared)
存在暂时性死区
在一个块级作用域中,变量唯一存在,一旦声明了一个,就属于这个块级作用域,不受外部的影响
本质就是:只要已进入当前作用域,所要用的变量就已经存在了,但是不可以获取,只要等到声明变量那行开始才可以获取使用
意义:标准化代码,将所有的变量声明放在最前面
var a=1; if(1){ console.log(a); let a=2; } //cannot access 'a' before initialization
const
- 一般用来声明常量,不允许修改
- 只读属性,声明同时就要赋值
- 和let一样,都是块级作用域,存在暂时性死区,没有变量声明提前,不允许重复定义
var
- var的作用域是函数作用域,在一个函数内部利用var声明一个变量,则这个变量只在函数内有效
- 存在变量声明提前(但是赋值并没有提前,提前访问会返回undefined)
let存储的位置在哪
debug看了一下。
var 定义的变量存储在作用中全局变量(Global)中(也就是window)。
let定义的变量存储在于global同级的这个script的这个域中。
Promise
Promise是一种用于解决异步问题的思路、方案,简单说是个容器,里面存的是某个未来会结束的结果。是一个对象,可以获取异步操作的消息。有三种状态,pending resolved rejected ,状态变了就不能修改了。
在js里面,经常用异步的是ajax,比如sucess:一个回调,error一个回调。但是如果一次请求需要多个接口的时候就产生了回调地狱,promise可以用then来处理,它可以在一个then里面再写一个promise对象。
promise.all 多任务并行,输出失败的那个,如果成功,返回所有的执行结果。
promise.race 多任务执行,返回最先执行结束的任务结果。
Generator+yield
是ES6里面的新数据类型,像一个函数,可以返回多次。特点就是函数有个*号。
调用的话就是不断调用next() 返回当前的value 值 done的状态
return() 直接忽略所有yield ,返回最终的结果
可以随心所欲的交出和恢复函数的执行权。
await+async
async函数返回的是一个promise对象,await用于等待一个async函数的返回值。
优势在于处理then链。如果是多个Promise组成的then链,那么优势就比较明显了,可以用Promise来解决多层回调的题,可以进一步优化它。
Proxy
Proxy 是 ES6 中新增的功能,它可以用来自定义对象中的操作。
let p = new Proxy(target, handler)
target
代表需要添加代理的对象,handler
用来自定义对象中的操作,比如可以用来自定义 set
或者 get
函数。
接下来我们通过 Proxy
来实现一个数据响应式
let onWatch = (obj, setBind, getLogger) => { let handler = { get(target, property, receiver) { getLogger(target, property) return Reflect.get(target, property, receiver) }, set(target, property, value, receiver) { setBind(value, property) return Reflect.set(target, property, value) } } return new Proxy(obj, handler) } let obj = { a: 1 } let p = onWatch( obj, (v, property) => { console.log(`监听到属性${property}改变为${v}`) }, (target, property) => { console.log(`'${property}' = ${target[property]}`) } ) p.a = 2 // 监听到属性a改变 p.a // 'a' = 2
在上述代码中,我们通过自定义 set
和 get
函数的方式,在原本的逻辑中插入了我们的函数逻辑,实现了在对对象任何属性进行读写时发出通知。
EventLoop
浏览器执行是有一个执行栈的,当遇到异步的代码时,会被挂起并在需要执行的时候加入到task队列里面,一旦执行栈为空,eventloop会从Task队列里拿出需要执行的代码放入执行栈中执行。执行完毕后就弹出。
但是不同的任务源会分到微任务和宏任务里面。
顺序:
首先执行同步代码,属于宏任务
执行完所有宏任务之后,查询有没有异步代码需要执行,然后执行微任务
执行微任务之后,如果有必要会重新渲染页面
然后一下轮执行宏任务中异步代码,比如说setTimeout的回调函数。
JS的链式调用
常见一般是三种,一种是jq直接返回this的方式,一种是underscore的可选式方式,lodash惰性求值的方式。
jq直接返回this
function A(num){ this.value=num||0; } //运算并返回this A.prototype.add=function(a){this.value+=a;return this;} A.prototype.reduce=function(a){ this.value+=a;return this;} //为了显示正常,修改两个继承的方法 A.prototype.valueOf=function(){return this.value;} A.prototype.toString=function(){return this.value+'';} //验证 let a=new A(2); console.log(a.add(1).reduce(2));
undersocre中的链式调用
undersocre规定了两种调用方式,_.forEach(arr,fn)
;_.map(arr,fn)
和_.chain(arr).forEach(fn).map(fn)
第一步:先实现forEach,map的功能
//对数组进行处理 var _={}; _.forEach=function(array,fn){ array.forEach(function(v,i,array){ fn.apply(v,[v,i,array]) }) }; _.map=function(array,fn){ return array.map(function(v,i,array){ return fn.apply(v,[v,i,array]); }) }
用ES5数组原型的方法实现了,接下来来实现链式调用。
在第二种调用方式中,所有的操作都是在_.chain(arr)
上调用的,所以_.chain(arr)
应该是返回了一个对象,这个对象上有和_
上相同的方法,只是实现上传参由2个变成了1个。
所以可以修改下上面的代码:
var _=function(array){ this._value=Array.prototype.slice.apply(array); }; _.forEach=function(array,fn){ array.forEach(function(v,i,array){ fn.apply(v,[v,i,array]); }) }; _.map=function(array,fn){ return array.map(function(v,i,array){ return fn.apply(v,[v,i,array]); }) }; _.chain=function(array){ return new _(array); }
新的构造函数有了,但它新生成的对象除了_value
就是一片空白,接下来要把原本_
上的方法移植到生成的对象上。
for(var i in _){//遍历_ if(i !=='chain'){ _.prototype[i]=(function(i){ //把其他方式经过处理赋值给_.prototype return function(){//通过闭包把i转化为局部变量 var args=Array.prototype.slice.apply(arguments);//取出fn args.unshift(this._value);//把_value放入参数数组的第一位 if(i==='map'){//map 需要修改_value值 this._value=_[i].apply(this,args); }else{ //forEach不修改 _[i].apply(this,args); } return this; } })(i) } }
最后模仿underscore使用value返回当前的_value
。
_.prototype.value=function(){ return this._value; }
接下来,来验证一下:
let a=[1,2,3]; _.forEach(a,function(v){console.log(v)})//1 2 3 _.map(a,function(v){return ++v;})//[2,3,4] _.chain(a).map(function(v){return ++v;}).foreach(function(v){console.log(v)}).value(); //2 3 4 //[2,3,4]
loadash惰性链式调用
首先说一下什么是惰性调用,比如上面的_.chain(arr).forEach(fn).map(fn).value()
,当执行道chain(arr)
的时候,返回了一个对象,执行到forEach的时候开始轮询,轮询完了之后,又返回到这个对象,执行到map的时候再次轮询,轮询完又返回这个对象,最后执行到value,返回对象中_value的值。
以上的每一步都是独立,依次进行的。
惰性调用就是,执行到forEach的时候不执行轮询操作,把操作塞进队列,执行到map的时候,再把map操作塞进队列。当某个特定的操作塞进队列的时候才开会执行之前队列中所有的操作。比如当value被调用时,才开始执行forEach、map和value。
惰性调用的好处是,比如说obj.job1().job2().job3()。如果需要连续执行100遍job1-3,那么就需要写100遍,或者用for把整个链条断开100次。所以传统的链式操作缺点很明显,函数链中都是job,不存在controller,如果用惰性调用的话,就是obj.loop(100).job1().job2().job3().end().done()。其中loop是开始循环,end是结束循环,done是执行。
现在来实现一下惰性链式调用:
首先要有个构造函数,生成可供链式调用的对象,任何cotroller或者job的调用都是把它塞入任务队列,这个构造函数自然要有个队列属性。如果要实现loop,那么还有loop的总次数和当前loop的次数,需要记录一下loop开始地方。
function Task() { this.queen = []; this.queenIndex = 0; this.loopCount = 0; this.loopIndex = 0; this.loopStart = 0; }
现在开始实现controller和job。他们应该包含两种形态,一种是本来的业务逻辑,一种形态是不管业务逻辑做什么,把业务逻辑响应的代码统一塞进任务队列。
如果最终调用格式是new Task().loop(100).job1().job2().end().done()。那么方法链上的方法是包装器,这些方法应该放在Task.prototype上,那么第一种形态的方法该何去何从呢?就放在Task.prototyle._proto_
上吧.
var _task_proto = { loop: function(num) { this.loopStart = this.queenIndex; this.loopCount = num; }, job: function(str) { console.log(str); }, end: function() { this.loopIndex++; if(this.loopIndex < this.loopCount) { this.queenIndex = this.loopStart; }else { this.loopIndex = 0; } }, done: function() { console.log('done'); } }; Task.prototype.__proto__ = _task_proto;
接下来,在Task,prototype上定义一个新的方法,这个方法专门用来控制任务执行,因为任务队列是依次执行并且由索引定位,和迭代器有点像,定义这个方法叫next
Task.prototype.next = function() { var task = this.queen[this.queenIndex]; //取出新的任务 task.fn.apply(this, task.args); //执行任务中指向的具体的实现方法,并传入之前保存的参数 if(task.name !== 'done') { this.queenIndex++; this.next(); //如果没执行完,任务索引+1并再次调用next }else { this.queen = []; this.queenIndex = 0; //如果执行完了,清空任务队列,重置任务索引 } }
之后遍历_task_proto
在Task.prototype上生成包装器,并让每个包装器返回this以供链式调用(每种都需要这样做),并且添加next,以便让任务队列执行。
for(var i in _task_proto){ (function(i){ var raw=Task.prototype[i]; Task.prototype[i]=function(){ this.queen.push({ name:i, fn:raw, args:arguments });//保存实现的方法、名字和参数到任务队列 if(i==='done'){ this.next(); } return this; } })(i); }
最后进行验证
var t=new Task(); t.job('P').loop(3).job('handsome').end().loop(3).job('cool').end().job('!').done(); //P handsome handsome handsome cool cool cool ! done
面试题
实现a().b().c()
function a(){ return { b:function(){ return { c:function(){ console.log('a'); } } } } } a().b().c(); //a
实现 o.a().b().c()
var temp={ a:function(){console.log('a');return this;}, b:function(){console.log('b');return this;}, c:function(){console.log('c');return this;} } var o=new Object(); o.__proto__=temp; o.a().b().c(); //a b c
数组
API有哪些?
直接修改原数组的:push(),unshift(), pop(),shift(), splice() ,reverse(), sort() fill(x,start,end) copyWithin(tochangeindex,changestart,changeend)
返回新数组的: concat(),slice()
返回字符串: join()
位置或是否在数组内: indexOf() lastindexOf() includes()是否包括 find()满足条件的索引;
遍历方法:forEach()所有元素执行一次,返回undefined;map()返回值新数组;filter()返回通过元素的新数组 ;every()所有都满足返回true;some()只要有一个元素满足就返回true;reduce(fn(pre,cur,index,arr),basevalue)累加器;reduceRight()从右边往左边加
迭代器:arr.keys()返回索引迭代器;arr.values()返回迭代器,值;arr.entries()返回键值对。
数组的sort方法底层如何排序?
谷歌浏览器:大于22的数组 快排;小于22的用插入排序
火狐浏览器:归并排序
webkit:用的c++的qsort();