fanqifeng

fanqifeng 查看完整档案

苏州编辑苏州大学  |  网络工程 编辑  |  填写所在公司/组织 www.fanqifeng.com 编辑
编辑

javascript语言爱好者,喜欢发掘js中的特异之处。现阶段沉迷购书无法自拔。

个人动态

fanqifeng 发布了文章 · 2018-01-10

了解HTML5中的MutationObserver

MutationObserver翻译过来就是变动观察器,字面上就可以理解这是用来观察Node(节点)变化的。MutationObserver是在DOM4规范中定义的,它的前身是MutationEvent事件,该事件最初在DOM2事件规范中介绍,到来了DOM3事件规范中正式定义,但是由于该事件存在兼容性以及性能上的问题被弃用。

MutationEvent

虽然MutationEvent已经被弃用,但是我们还是需要了解它,可能你会为了浏览器兼容性的问题而遇到它(万恶的浏览器兼容性)。

MutationEvent总共有7种事件:DOMNodeInsertedDOMNodeRemovedDOMSubtreeModifiedDOMAttrModified
DOMCharacterDataModifiedDOMNodeInsertedIntoDocumentDOMNodeRemovedFromDocument

MutationEvent的兼容性:

  1. MutationEvent在IE浏览器中最低支持到IE9
  2. 在webkit内核的浏览器中,不支持DOMAttrModified事件
  3. IE,Edge以及Firefox浏览器下不支持DOMNodeInsertedIntoDocumentDOMNodeRemovedFromDocument事件

MutationEvent中的所有事件都被设计成无法取消,如果可以取消MutationEvent事件则会导致现有的DOM接口无法对文档进行改变,比如appendChild,remove等添加和删除节点的DOM操作。
MutationEvent中最令人诟病的就是性能以及安全性的问题,比如下面这个例子:

document.addEventListener('DOMNodeInserted', function() {
    var newEl = document.createElement('div');
    document.body.appendChild(newEl);
});

document下的所有DOM添加操作都会触发DOMNodeInserted方法,这时就会出现循环调用DOMNodeInserted方法,导致浏览器崩溃。还有就是MutationEvent是事件机制,因此会有一般事件都存在的捕获和冒泡阶段,此时如果在捕获和冒泡阶段又对DOM进行了操作会拖慢浏览器的运行。

另一点就是MutationEvent事件机制是同步的,也就是说每次DOM修改就会触发,修改几次就触发几次,严重降低浏览器的运行,严重时甚至导致线程崩溃

<div id='block'></div>
var i=0;
block.addEventListener('DOMNodeInserted', function(e) {
     i++                                  
});
block.appendChild(docuemnt.createTextNode('1'));
console.log(i)                  //1
block.appendChild(docuemnt.createTextNode('2'));
console.log(i)                  //2
block.appendChild(docuemnt.createTextNode('3'));
console.log(i)                  //3

再看个例子:

<div id='block'>
  <span id='span'>Text</span>
</div>
block.addEventListener('DOMNodeInserted', function(e) {
     console.log('1');                                  //1
});
span.appendChild(docuemnt.createTextNode('other Text'));

span元素中添加节点会触发block中的DOMNodeInserted事件,可是你只想观察block的变化,不想观察block中子节点的变化,这时你不得不在DOMNodeInserted事件中进行过滤,把对span的操作忽略掉,这无疑增加了操作的复杂性。

MutationObserver

MutationObserver的出现就是为了解决MutationEvent带来的问题。
先看一下MutationObserver的浏览器兼容性:

MutationObserver浏览器兼容性

我们可以看到MutationObserver在IE中最低要就是IE11,如果你的网站不需要支持IE或者只支持到IE11,那么你可以放心的使用MutationObserver,否则你可能需要用到上面提到的MutationEvent事件,当然如果你的网站还要支持IE8及以下版本,那么你只能和Mutation说拜拜了。

MutationObserver是一个构造器,接受一个callback参数,用来处理节点变化的回调函数,返回两个参数,mutations:节点变化记录列表(sequence<MutationRecord>),observer:构造MutationObserver对象。

var observe = new MutationObserver(function(mutations,observer){
})

MutationObserver对象有三个方法,分别如下:

  1. observe:设置观察目标,接受两个参数,target:观察目标,options:通过对象成员来设置观察选项
  2. disconnect:阻止观察者观察任何改变
  3. takeRecords:清空记录队列并返回里面的内容

关于observe方法中options参数有已下几个选项:

  1. childList:设置true,表示观察目标子节点的变化,比如添加或者删除目标子节点,不包括修改子节点以及子节点后代的变化
  2. attributes:设置true,表示观察目标属性的改变
  3. characterData:设置true,表示观察目标数据的改变
  4. subtree:设置为true,目标以及目标的后代改变都会观察
  5. attributeOldValue:如果属性为true或者省略,则相当于设置为true,表示需要记录改变前的目标属性值,设置了attributeOldValue可以省略attributes设置
  6. characterDataOldValue:如果characterData为true或省略,则相当于设置为true,表示需要记录改变之前的目标数据,设置了characterDataOldValue可以省略characterData设置
  7. attributeFilter:如果不是所有的属性改变都需要被观察,并且attributes设置为true或者被忽略,那么设置一个需要观察的属性本地名称(不需要命名空间)的列表

下表描述了MutationObserver选项与MutationEvent名称之间的对应关系:

MutationEventMutationObserver options
DOMNodeInserted{ childList: true, subtree: true }
DOMNodeRemoved{ childList: true, subtree: true }
DOMSubtreeModified{ childList: true, subtree: true }
DOMAttrModified{ attributes: true, subtree: true }
DOMCharacterDataModified{ characterData: true, subtree: true }

从上表我们也可以看出相比与MutationEvent而言MutationObserver极大地增加了灵活性,可以设置各种各样的选项来满足程序员对目标的观察。

我们简单看几个例子:

<div id='target' class='block' name='target'>
    target的第一个子节点
    <p>
       <span>target的后代</span>
    </p>
</div>

1.callback的回调次数

var target=document.getElementById('target');
var i=0
var observe=new MutationObserver(function (mutations,observe) {
    i++   
});
observe.observe(target,{ childList: true});
target.appendChild(docuemnt.createTextNode('1'));
target.appendChild(docuemnt.createTextNode('2'));
target.appendChild(docuemnt.createTextNode('3'));
console.log(i)                //1

MutationObserver的callback回调函数是异步的,只有在全部DOM操作完成之后才会调用callback。

2.当只设置{ childList: true}时,表示观察目标子节点的变化

var observe=new MutationObserver(function (mutations,observe) {
    debugger;
    console.log(mutations);
    //observe.discount();     
});

observe.observe(target,{ childList: true});
target.appendChild(document.createTextNode('新增Text节点'));   //增加节点,观察到变化
target.childNodes[0].remove();                                //删除节点,可以观察到
target.childNodes[0].textContent='改变子节点的后代';             //不会观察到

如果想要观察到子节点以及后代的变化需设置{childList: true, subtree: true}

attributes选项用来观察目标属性的变化,用法类似与childList,目标属性的删除添加以及修改都会被观察到。

3.我们需要注意的是characterData这个选项,它是用来观察CharacterData类型的节点的,只有在改变节点数据时才会观察到,如果你删除或者增加节点都不会进行观察,还有如果对不是CharacterData类型的节点的改变不会观察到,比如:

observe.observe(target,{ characterData: true, subtree: true});
target.childNodes[0].textContent='改变Text节点';              //观察到
target.childNodes[1].textContent='改变p元素内容';              //不会观察到
target.appendChild(document.createTextNode('新增Text节点'));  //不会观察到
target.childNodes[0].remove();                               //删除TEXT节点也不会观察到

我们只需要记住只有对CharacterData类型的节点的数据改变才会被characterData为true的选项所观察到。

4.最后关注一个特别有用的选项attributeFilter,这个选项主要是用来筛选要观察的属性,比如你只想观察目标style属性的变化,这时可以如下设置:

observe.observe(target,{ attributeFilter: ['style'], subtree: true});
target.style='color:red';                      //可以观察到
target.removeAttribute('name');                //删除name属性,无法观察到 

disconnect方法是用来阻止观察的,当你不再想观察目标节点的变化时可以调用observe.disconnect()方法来取消观察。

takeRecords方法是用来取出记录队列中的记录。它的一个作用是,比如你对一个节点的操作你不想马上就做出反应,过段时间在显示改变了节点的内容。

var observe=new MutationObserver(function(){});
observe.observe(target,{ childList: true});
target.appendChild(document.createTextNode('新增Text节点'));
var record = observe.takeRecords();              //此时record保存了改变记录列表  
//当调用takeRecords方法时,记录队列被清空因此不会触发MutationObserver中的callback回调方法。
target.appendChild(document.createElement('span'));
observe.disconnect();                            //停止对target的观察。
//MutationObserver中的回调函数只有一个记录,只记录了新增span元素

//之后可以对record进行操作
//...

MutationRecord
变动记录中的属性如下:

  1. type:如果是属性变化,返回"attributes",如果是一个CharacterData节点(Text节点、Comment节点)变化,返回"characterData",节点树变化返回"childList"
  2. target:返回影响改变的节点
  3. addedNodes:返回添加的节点列表
  4. removedNodes:返回删除的节点列表
  5. previousSibling:返回分别添加或删除的节点的上一个兄弟节点,否则返回null
  6. nextSibling:返回分别添加或删除的节点的下一个兄弟节点,否则返回null
  7. attributeName:返回已更改属性的本地名称,否则返回null
  8. attributeNamespace:返回已更改属性的名称空间,否则返回null
  9. oldValue:返回值取决于type。对于"attributes",它是更改之前的属性的值。对于"characterData",它是改变之前节点的数据。对于"childList",它是null

其中 typetarget这两个属性不管是哪种观察方式都会有返回值,其他属性返回值与观察方式有关,比如只有当attributeOldValue或者characterDataOldValue为true时oldValue才有返回值,只有改变属性时,attributeName才有返回值等。

查看原文

赞 47 收藏 35 评论 7

fanqifeng 回答了问题 · 2017-12-29

解决css中fixed如何定位在屏幕的正中间,并在周围生成一个半透明的黑色遮罩??

<div class='center-block'>
   <span class='Hello World'>
</div>

居中显示:
1.如果div的宽高已知

.center-block{
    position:fixed;
    left:50%;
    top:50%;
    width:200px;
    height:100px;
    margin-left:-100px;
    margin-top: -50px;
}

2.如果div的宽度未知

.center-block{
    position:fixed;
    left:50%;
    top:50%;
    transform: translate(-50%,-50%);
}

至于阴影遮罩

.center-block{
  z-index:3   
}
.center-block:after{
    content: "";
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    background: rgba(0,0,0,.5);
    z-index: 2;   //z-index比div小
 }

关注 6 回答 5

fanqifeng 回答了问题 · 2017-12-25

解决y.push is not a function ?为什么呢?

数组的push方法返回的是数组的长度不是一个数组。

y += y.push(dict[x]); //相当于
y += dict[x].length + 1;  //这句话操作完之后y已经不是一个数组了,所以再次循环找不到push方法

关注 9 回答 9

fanqifeng 回答了问题 · 2017-12-25

解决关于javascript,私有作用域中this的指向问题,谢谢各位!!

第三句话是赋值表达式,返回的是赋值后的结果。比如a = b,这只是一个赋值语句,把b中引用的值赋给了a,操作完之后就结束了,之后跟a是什么没有关系。主要是赋值表达式有副作用,它返回了b引用的值。比方说b的值是1,那么a = b它的返回结果就是1。

//这句话操作完成返回的结果是obj.sayName的引用
(obj.sayName = obj.sayName)();
//相当于如下
(function(){
    alert(this.name);
})();
//匿名函数表达式立即执行,this指向window

关注 5 回答 3

fanqifeng 回答了问题 · 2017-12-25

解决js循环套循环怎么能减少代码量

试试这个

class Foo{
    constructor(){
        this.arr_select=[3,1,2,6,8,10,5];
        this.tableData=[{id:12},{id:2},{id:10},{id:1},{id:3},{id:5},{id:4}]
    }
    delAll(){        
     this.tableData=this.tableData.filter(j=>!this.arr_select.find(v=>v===j.id,this))
    }
}
let foo = new Foo();
foo.delAll();
console.log(foo.tableData)

关注 7 回答 6

fanqifeng 回答了问题 · 2017-12-20

解决关于两个数组进行比较的问题 js

var array_A = [{number:529,NAME:"甲"},{number:550,NAME:"乙"},{number:593,NAME:"丙"},{number:813,NAME:"丁"}];
var array_B = [{ID:"201712015041",number:529},{ID:"201712015045",number:550},{ID:"201712015031",number:593},{ID:"201712015039",number:558}];
var array_C = array_B.map(item=>{
    let _item = array_A.find(item1=>item1.number===item.number);
    if(_item){
        _item.ID = item.ID;
        return _item;
    }else{
        item.NAME = item.number+'';
        return item;
    }
});
console.log(array_C)

关注 8 回答 7

fanqifeng 发布了文章 · 2017-12-14

javascript中数组的回顾

数组在javascript中使用度非常频繁,我总结了一些在数组中很常见的问题。

关于数组中的方法非常多,我总结了一张表来大致了解数组中的方法

Array中的方法含义改变原数组返回值ES6新增
concat合并两个或多个数组false新数组false
copyWithin浅复制数组的一部分到同一数组中的另一个位置true改变后的数组true
entries返回数组迭代器对象,该对象包含数组中每个索引的键/值对false数组迭代器true
every测试数组的所有元素是否都通过了指定函数的测试false布尔值,true/falsefalse
fill用一个固定值填充一个数组中从起始索引到终止索引内的全部元素true改变后的数组true
filter创建一个新数组, 其包含通过所提供函数实现的测试的所有元素false新数组false
find返回数组中满足提供的测试函数的第一个元素的值。否则返回undefinedfalsejavascript语言类型true
findIndex返回数组中满足提供的测试函数的第一个元素的索引。否则返回-1false数组索引true
forEach遍历数组falseundefinedfalse
includes判断一个数组是否包含一个指定的值false布尔值,true/falsetrue
indexOf返回在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回-1false数组索引false
join将数组(或一个类数组对象)的所有元素连接到一个字符串中false字符串false
keysArray迭代器,它包含数组中每个索引的键false数组迭代器true
lastIndexOf返回指定元素在数组中的最后一个的索引,如果不存在则返回 -1false数组索引false
map遍历数组false新数组false
pop从数组中删除最后一个元素,并返回该元素的值true数组元素false
push将一个或多个元素添加到数组的末尾,并返回新数组的长度true数组长度false
reduce对累加器和数组中的每个元素(从左到右)应用一个函数,将其减少为单个值false函数返回值false
reduceRightreduce执行方向相反,从右到左false函数返回值false
reverse将数组中元素的位置颠倒true改变后的数组false
shift从数组中删除第一个元素,并返回该元素的值true数组元素false
slice可从已有的数组中返回选定的元素false新数组false
some测试数组中的某些元素是否通过由提供的函数实现的测试false布尔值,true/falsefalse
sort在适当的位置对数组的元素进行排序true一个新数组false
splice删除现有元素和/或添加新元素来更改一个数组的内容true删除的元素数组false
toLocaleString返回一个字符串表示数组中的元素false字符串false
toString返回一个字符串,表示指定的数组及其元素false字符串false
unshift将一个或多个元素添加到数组的开头true数组长度false
values一个数组迭代器对象,该对象包含数组每个索引的值false数组迭代器true

从这个表中我们要小心几个方法,reverse和sort会改变原数组,并返回改变的新数组,push和unshift方法返回的是数组长度而不是数组,forEach方法返回的是undefined不是数组。

此外,我还需提一下slice和splice这两个方法,说实话这两个方法看起来很像,容易让人搞混,最关键的是用到的频率还蛮高的,这两个方法就像字符串中substr和substring这两个老兄弟,闲着没事就喜欢去迷惑别人,本人就曾深深的被这两个方法伤害过。

slice接受两个参数start和end,代表需要截取的数组的开始序号和结束序号。

var arr = [4,3,5,8,9,6];
arr.slice(0)    // [4,3,5,8,9,6],end可以省略,默认为数组长度
arr.slice(0,4)   //[4,3,5,8]
arr.slice(-1);   //[6],  start为负数代表从数组截取的开始序号从尾部算起
arr.slice(0,-1);  //[4,3,5,8,9]   end为负数表示结束序号从尾部算起
arr.slice(2,0);   //[]
arr.slice(-1,-1);   //[]  如果start和end符号相同,end一定大于start,否则返回的会是[]

splice的参数为index,deleteCount和...items,index表示需要删除或添加原数时的位置,负数表示从尾部算起,deleteCount表示要删除的元素,0表示不删除。其中items表示添加的元素个数。

var arr = [4,3,5,8,9,6];
arr.splice(0,0)      //返回[], arr=[4,3,5,8,9,6];
arr.splice(0,2)      //返回[4,3], arr=[5,8,9,6];
arr.splice(0,2,3,4)  //返回[5,8], arr=[3,4,9,6];

splice不管是添加还是删除元素,返回的都是删除元素的列表,splice是先做删除操作,后添加

var arr = [4,3,5];
arr.splice(3,1,8,9);     //返回[], arr= [4, 3, 5, 8, 9];
//如果index大于数组长度,那么splice不会删除元素

注意:虽然slice和splice都返回一个新的数组,但是slice不会改变原数组,splice会改变原数组,这个区别非常关键。

最后在加一些经常会问到的数组问题。

1.创建数组

//数组字面量创建
var arr = [1,2];

//Array构造器创建;
var arr = Array(1,2);     //[1,2]  可以用new操作符,也可以不用
//Array构造器有个局限性,不能创建只有单个数字的数组
var arr = Array(10)       //创建的是一个长度为10的空数组,并不是[10]
//如果传入的不是Number类型的数字,那么没有任何问题
var arr = Array('10')     //['10']

//此时如果要创建只有单个数字的数组,可以用Array.of方法
var arr = Array.of(10)    //[10]
var arr = Array.of(1,2)   //[1,2]

//Array.from( items [ , mapfn [ , thisArg ] ] )
//items是个可迭代对象,mapfn是遍历该迭代对象的function,thisArg是mapfn中的this对象
var arr = Array.from([1,2,3])    //[1,2,3]

Array.from是非常有用的创建数组方法,能把字符串转化为数组,Map,Set也能转成数组。

Array.from('abc')        //['a','b','c'];
Array.from(new Set([1,2,3]))  //[1,2,3],当然这个例子毫无意义
Array.from(new Map([[1,2],[3,4]]))  //[[1,2],[3,4]]

我们知道用Array构造器创建的数组是个空数组,map,forEach方法并不能遍历这个数组。

var arr = Array(10);
arr.forEach((item,index) => console.log(index))   //不会有输出
//map,forEach循环判断的是该对象上有没有对应的属性
arr.hasOwnProperty(0)            //false,以hasOwnProperty为判断标准
//Array.from中的mapfn方法是以迭代方式来判断的,因此
Array.from(arr,(item,index)=>console.log(index))   //0,1,2,3,4,5,6,7,8,9

由于这个原因,我们可以快速对数组初始化,比如创建一个0到99的数组

Array.from(Array(100),(item,index)=>index);
//当然,如果你用到上表中Array的keys方法那更快捷
Array.from(Array(100).keys());

2.数组去重

方法一,创建对象的键值唯一性来进行去重:

var arr = [1,2,3,1,3,5,3,2];
var _arr = [];
var obj = {};
arr.forEach(item => {
    if(!obj[item]){
       _arr.push(item);
       obj[item] = true;
    }
})
arr = _arr;

方法二,结合Set的键值唯一性以及Array.from方法可以快速数组去重:

var arr = Array.from(new Set([1,2,3,1,3,5,3,2]))   //[1,2,3,5]

3.快速复制一个数组

var arr = [1,2,3,4];
var arr1 = arr.slice();
var arr2 = arr.concat();
注:这里的复制指的是浅拷贝

4.求数组最大值,最小值

这里的数组指的是全是数字的数组

方法一,sort排序后取值

var arr = [1,4,6,2,33,19,6,9];
var maxvalue = arr.sort((a,b) => b>a )[0]
var minvalue = arr.sort((a,b) => a>b )[0]

方法二,Math的max和min方法调用

var arr = [1,4,6,2,33,19,6,9];
var maxvalue = Math.max.apply(null,arr);   //33
var minvalue = Math.min.apply(null,arr);   //1

5.数组排序

在不用系统自带的sort的情况下对数组排序有很多方法,比如冒泡、插入以及快速排序等。但我总觉得这些排序方法还是过于复杂,有没有更快以及更方便的排序,我思考了好久,后来先想到了可以用数组的序号进行排序。原理是把数组1中的值变成数组2中的序号:

var arr = [3,4,6,2,8,7,5],
    arr2 = [];
arr.forEach(item => arr2[item] = item);
arr = [];
arr2.forEach(item => arr.push(item));  

写完之后自己感觉美滋滋,可之后发现如果数组中有负数,不就都玩完了吗。于是赶紧改:

var arr = [3,-4,6,-2,-8,7,5],
    parr = [];
    narr = [];
arr.forEach(item => item>=0?parr[item] = item:narr[-item] = item);
arr = [];
parr.forEach(item => arr.push(item));
narr.forEach(item => arr.unshift(item));    
注:如果数组中有重复数字则排序方法有误,会把重复数字去掉。

写完之后发现其实也没有比冒泡、插入以及快速排序的方法快多少。

6.求一个整数数组是否包含另一个整数数组

一开始我想到一个方法,把两个数组转换成字符串,在进行includes或者indexOf判断就可以了,后来我发现了问题:

var a = [2,4,8,6,12,67,9];
var b = [8,6,12];
a.join(',').includes(b.join(','));   //true;  这是可以的

var b = [8,6,1]
a.join(',').includes(b.join(','));   //true;  这居然也可以,显然有问题

//于是改成
a.join(',').includes(','+b.join(',')+',');  //false;

//后来我又发现如果b数组在a数组的开头和结尾都会有问题,于是又改成如下:

(','+a.join(',')+',').includes(','+b.join(',')+',');  //false;

写这篇文章主要是对自己学习数组做一个总结。如果对上面的问题有更好的解答,欢迎留言告知。

查看原文

赞 1 收藏 2 评论 1

fanqifeng 回答了问题 · 2017-12-14

解决为什么使用typeof 查看类型function会是独立的类型。

function本质上也是一个对象,但是function对象与普通对象相比,其内部有一个[[Call]]方法,用来表示这个对象是可调用的,typeof操作符在判断Object时,如果内部实现了[[Call]]方法,就返回function。
附上typeof操作符返回判断图:
图片描述

关注 4 回答 3

fanqifeng 发布了文章 · 2017-12-12

javascript中Function、ArrowFunction和GeneratorFunction介绍

ECMAScript规范中对Function的文档描述,我认为是ECMAScript规范中最复杂也是最不好理解的一部分,它涉及到了各方面。光对Function就分了Function Definitions、Arrow Function Definitions、Method Definitions、Generator Function Definitions、Class Definitions、Async Function Definitions、Async Arrow Function Definitions这几块。我准备花三章来介绍Function。这篇文章主要是理解ArrowFunction和GeneratorFunction,当然还包括最基本最普通的Function Definitions。

Function

在了解Function Definitions之前我们需要知道函数对象(Function Object)。我们都知道Function本质上也是一个对象,所以普通对象有的方法Function对象都有,此外Function对象还有自己的内部方法。所有Function对象都有一个[[Call]]的内部方法,有了这个方法Function对象才能被用作函数调用,即你***()时内部调用就是[[Call]]方法,当然不是所有有[[Call]]方法的Function对象都可以进行***()调用,常见的Map、Set等方法虽然有[[Call]]方法,但是你不能进行Map()和Set(),这时候就用到了Function对象的另一个内部方法[[Construct]],当Function作为构造函数调用时,就会使用[[Construct]]方法。

注意:不是所有Function对象都有[[Construct]]方法。只有当Function作为构造函数调用时,才会有[[Construct]]方法,比如ArrowFunction和GeneratorFunction只有[[Call]]方法没有[[Construct]]方法。

[[Call]]

先说[[Call]]方法,看到这个名字很容易让人想起Function.prototype中的call方法,没错Function.prototype.call以及Function.prototype.apply都是显示的调用了[[Call]]方法,之所以说显示调用是相比于***()调用,call和apply要简单直接的多。[[Call]]方法接受两个参数,一个是thisArgument,另一个是argumentsList,thisArgument即表示了function中的this对象,argumentsList代表了function中的参数列表,看!和Function.prototype.apply的调用方式是如此的相似。

function foo(){}

foo(1,2)     //当执行foo()方法时,实际上内部是如下调用

foo.[[Call]](undefined,« 1, 2 »)   //« »表示ECMAScript的List规范类型

//注意,如果你是隐式调用function,那么thisArgument是undefined,不是常说的全局对象window,
//只是在[[Call]]内部执行时检测到如果thisArgument是undefined或null,
//且在非严格模式下才会变成全局对象,即foo(1,2)你可以认为等价与下面的:

foo.call(null,1,2)
foo.apply(undefined,[1,2])
//-------------------
var a={
   foo:function(){}
}

a.foo(1,2)   
//等价与==>   foo.[[Call]](a,« 1, 2 »)
//等价与==>   foo.call(a,1,2)
//等价与==>   foo.apply(a,[1,2])

这里有个建议,以后你遇到this指向问题的时候,你把function转成call或者apply模式,你就能清楚的明白this指向什么。

[[Construct]]

[[Construct]]内部方法主要有new关键字调用Function时才会执行[[Construct]]方法。[[Construct]]方法主要接受两个参数一个是argumentsList, 还有一个是newTarget。newTarget正常调用下指向调用的function对象。比如foo(),newTarget就是foo,你可以在函数内部用new.target访问到。构造函数中的this对象与newTarget有关,如果newTarget.prototype存在,且是Object对象,则this就是ObjectCreate(newTarget.prototype),ObjectCreate是Object.create内部调用的方法,如果newTarget.prototype不存在或者不是Object对象,this相当于ObjectCreate(Object.prototype)。

function Foo(){}

var fooInstance = new Foo(1,2)
//等价与==>  var fooInstance = Foo.[[Construct]](« 1, 2 »,Foo);

fooInstance instanceof Foo   //true

Object.create(Foo.prototype) instanceof Foo    //true

//注意如果构造函数有自己的return返回,那么情况有所不同。
//返回的是Object,则构造函数的实例就是返回的对象
//返回的不是Object,相当于默认没有返回

function Foo(){ return {a:1}}

var fooInstance = new Foo(1,2)

fooInstance instanceof Foo   //false,注意不是true,fooInstance不是Foo的实例

Object.create(Foo.prototype) instanceof Foo    //true

//只要Foo.prototype存在且是对象,那么Object.create(Foo.prototype)永远是Foo的一个实例

Function Definitions

Function Definitions包含了FunctionDeclaration和FunctionExpression,有一些早期错误检测添加到Function Definitions中,其中在function中的let、const和var声明的变量规则参考上一篇文章var、let、const声明的区别,另外有一些附加的早期错误:

function中的参数被认为是var声明,因此:

function foo(a,b){
    let a = 1;     //SyntaxError,重复声明a
}
foo();

如果函数体是严格模式而参数列表不是简单参数列表,则语法错误:

//不是简单参数指的是包含解构赋值
function foo(a=1,...c){
    'use strict'      //SyntaxError
}

//如果'use strict'在函数体外定义则没有错误
'use strict' 
function foo(a=1,...c){}    //ok

函数体以及函数参数不能直接出现super

function foo(super){}   //SyntaxError

function foo(){ super();} //SyntaxError

FunctionDeclaration

FunctionDeclaration分为带变量名的函数声明以及匿名函数声明,匿名函数声明只能在export中可用,其它任何地方使用匿名函数声明都报错。
在进行评估脚本和函数的时候会对包含在其中的函数声明进行InstantiateFunctionObject方法,即初始化函数对象。注:该方法是在执行脚本和函数代码之前进行的。
InstantiateFunctionObject方法简单来说做了三步:1.FunctionCreate 2.makeConstructor 3. SetFunctionName。分开说

  1. FunctionCreate创建了一个Function对象F,包括初始化内部插槽的值,比如上面提到的[[Call]],[[Construct]]方法的定义,原型对象[[Prototype]]的值,这里指的是Function.prototype,F的length属性,指function的参数个数。
  2. makeConstructor(F),这句话不是指创建构造器,这里指定义了F的prototype属性的值,以及prototype中constructor的值,普通函数prototype相当于object.create(Object.prototype),constructor===F。
  3. SetFunctionName,定义了F的name属性,默认是声明的函数名,匿名函数是default。
function foo(a,b){};

foo.__proto__ === Function.prototype;
foo.length === 2;
foo.prototype.__proto__ === Object.prototype;
foo.prototype.constructor === foo;
foo.name === 'foo';

FunctionExpression

FunctionExpression也分为两类,有变量名的函数表达式和匿名函数表达式。函数表达式在执行时也会创建Function对象,步骤和函数声明相似。其中匿名函数表达式不会定义属性name,即不会执行第三步中的SetFunctionName。有变量名的函数表达式与函数声明以及匿名函数表达式的区别在于作用域链,我们都知道一旦函数表达式中定义了变量名,我们就可以在函数体内通过该变量名调用函数自身。可问题来了,该函数变量名是定义在哪里呢?函数外还是在函数内呢?

var func = function foo(){};
foo();              //Uncaught ReferenceError: foo is not defined
//显然没有在函数外定义函数表达式的变量名,那么是定义在函数内的?

//我提到过在全局作用域和函数作用域中,var、function声明的变量,let和const不能重复声明。
var func = function foo(){
    let foo = 1;   //ok,可见函数表达式的变量名也不是在函数内声明的。
};
foo();

看到这可能有人会认为函数表达式的变量名可能允许let和const进行覆盖。其实不是,有变量名的函数表达式在创建Function对象的时候,创建了一个匿名作用域,在该作用域中定义了函数表达式的变量名。按上面这个例子,foo函数的外部作用域并不是全局作用域,而是一个匿名作用域,匿名作用域的外部作用域才是真正的全局作用域。匿名函数表达式和函数声明都不会创建匿名作用域。

ArrowFunction

ArrowFunction(箭头函数)是ES6新增的一种新语法,主要是用来简化function的写法,更准确的说是简化匿名函数表达式的一种写法。因此匿名函数表达式的规则也适用于ArrowFunction,不过两者还是有区别的,ArrowFunction中没有规定不能直接出现super,也就是说在ArrowFunction中可以用super方法,其次ArrowFunction内部没有[[Construct]]方法,因此不能作为构造器调用,所以在创建Function对象时不执行makeConstructor方法。最重要一点就是ArrowFunction没有本地的this对象。
我们上面提道所有Function对象都有[[Call]]内部方法,接受this对象和参数列表两个字段。此外Function对象还有一个[[ThisMode]]内部属性,用来判断是ArrowFunction还是非ArrowFunction,如果是ArrowFunction,那么不管[[Call]]中传来的this是什么都会被丢弃。此外arguments, super和new.target和this也是一样的。我在以前的文章中稍微提到过ArrowFunction中的this对象,我在这重新讲一下:

var name = 'outer arrow';
var obj = {
    name:'inner arrow',
    arrow: () => {
        console.log(this.name)
    }
}
obj.arrow();         //outer arrow,不是inner arrow

我们在ArrowFunction遇到this对象时,你不要把this看成是ArrowFunction的一部分,你从ArrowFunction中拿出this放到ArrowFunction的外部,观察外部的this对象是什么,外部的this对象就是ArrowFunction的this对象。此外还要清楚不管是call还是apply都是对ArrowFunction无效的,它们最终调用的都是[[Call]]内部方法,当然bind也是无效的。

我们看一下ArrowFunction中的super应用,还是改编了MDN中的例子:

var obj1 = {
  method() {
    console.log("method 1");
  }
}

var obj2 = {
  method() {
     console.log("method 2");
     return ()=>{super.method();}
  }
}

Object.setPrototypeOf(obj2, obj1);
var arrow = obj2.method()         //method 2
arrow();          //method 1

注意:method1和method2其实就是Method Definitions。

如果单看arrow这个函数,它本身是不可能有super的,因为没有任何继承关系,只是一个单一的ArrowFunction,但是你放在obj2中就有了意义,Object.setPrototypeOf(obj2, obj1);这句话把obj1变为obj2的原型对象,obj2继承了obj1的属性和方法,obj2的super对象就是obj1,因此ArrowFunction中的super参照this可知,该super是obj1。

总的一句话概括ArrowFunction中的this,arguments, super和new.target都是通过原型链来查找的,不是动态创建的。

GeneratorFunction

关于GeneratorFunction我不准备讲怎么用它,我只谈一下它的工作原理。说实话GeneratorFunction用到的情况实在太少了,我自己在做项目的时候基本不会用到GeneratorFunction,但这不妨碍我们学习GeneratorFunction。
GeneratorFunction也是Function的一种,Function的规则也适用于GeneratorFunction,此外在GeneratorFunction的参数中不能出现yield表达式。
GeneratorFunction与普通的Function一样,都会创建Function对象,但是区别也在这里,上面提到了Function的[[Prototype]]原型值是Function.prototype,但是GeneratorFunction不同,它的[[Prototype]]原型值是%Generator%,此外Function的prototype属性是ObjectCreate(Object.prototype),但是GeneratorFunction的却是ObjectCreate(%GeneratorPrototype%)而且prototype中没有constructor属性,不能作为构造器调用。

注:Function.prototype也写作%FunctionPrototype%,Object.prototype也写作%ObjectPrototype%。%Generator%和%GeneratorPrototype%没有全局名称,不能直接访问。

执行函数时,内部调用了[[Call]]方法,但是和Function不同的是GeneratorFunction返回的不是函数的执行结果,而是一个对象,这个对象是GeneratorFunction的一个实例,这跟[[Construct]]方法很像。

function* gen(){}
gen();    //返回的其实是Object.create(gen.prototype)对象。

你可以比较gen()和Object.create(gen.prototype)这两个对象,你会发现它们很像,只是Object.create(gen.prototype)缺少了Generator对象的一些内部状态。可以说虽然GeneratorFunction没有[[Construct]]方法,不能作为构造器调用,但是你可以认为GeneratorFunction本身就是一个构造器。

此外在创建GeneratorFunction对象时,还做了一些其他操作,我们在以前的文章中提到了执行上下文,GeneratorFunction对象有个[[GeneratorContext]]内部插槽,当评估GeneratorFunction定义的代码时,GeneratorFunction对象把当前正在运行的执行上下文存在了[[GeneratorContext]]中,并挂起了该正在运行的执行上下文,因此对GeneratorFunction中代码的评估被暂停了,从而执行其它代码,当你调用GeneratorFunction对象的next方法时,他把[[GeneratorContext]]中保存的执行上下文取出放到执行上下文栈顶部,成为正在运行的执行上下文,此时GeneratorFunction中暂定评估的代码又重新开始执行,直到执行完毕或者遇到yield表达式。当遇到yield表达式时,它又把正在运行的执行上下文从栈中移除,暂停对GeneratorFunction代码的执行,等待下次next方法调用之后继续执行。
简单来说GeneratorFunction的实现原理其实是运行的执行上下文之间不停来回切换。

GeneratorFunction基本就提到这里了,最后附上ECMAScript关于GeneratorFunction的一张关系图片:
图片描述

结束语

关于Function的简单介绍就暂时告一段落,在下一篇文章中我会来简单介绍Promise和AsyncFunction。

查看原文

赞 2 收藏 1 评论 0

fanqifeng 回答了问题 · 2017-12-11

解决关于ES6的Reflect

你用到Function.prototype.apply.call(fn, obj, args)这句话的前提是你自己重写了fn中的apply方法,比如:

fn.apply = function(){
    console.log(1);
}
//此时你想调用fn.apply(obj,args)肯定是不行的了,apply方法被覆盖了
//你不得不换个方法
Function.prototype.apply.call(fn, obj, args)
//分析这句话,你把Function.prototype.apply看成是一个函数fn2
fn2.call(fn, obj, args);
//把fn作为fn2的this对象,调用fn2()方法,参数是obj, args
//把fn看成对象,fn2作为fn的方法简化一下就是
fn.fn2(obj, args);
//此时fn2是Function.prototype.apply,应该知道所用函数中的apply方法其实都是调用了
//Function.prototype.apply方法,当然只要你不在函数中重写apply,比如
function foo(){}
foo.apply===Function.prototype.apply   //true
//因此
fn.fn2(obj, args); //等价于
fn.apply(obj, args);
//注意这里的apply是Function.prototype中的,不是上面fn中定义的apply方法

关注 2 回答 1

认证与成就

  • 获得 87 次点赞
  • 获得 4 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 3 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-11-03
个人主页被 557 人浏览