JavaScript闭包实现原理

JavaScript闭包实现原理

标签(空格分隔): js


什么是闭包?

闭包是指有权访问另外一个函数作用域的函数。


为什么需要闭包?

因为JavaScript没有修饰符(像java的private),只能通过函数作用域去模拟”私有”这个概念。


怎么样创建闭包?

创建闭包的常见方式就是在一个函数(外部函数)内返回一个匿名函数,而匿名函数则引用外部函数的私有变量(方法)。这就完成了类似”私有”的功能。
例如:

funtion foo(){
    var private=0;
    return function(){
        console.log(private++);
    }
}

以上代码创建了一个闭包。因此,闭包内的private变量就被闭包”私有”了,该私有变量在闭包生存期间并不会被回收,外部函数访问不了私有变量,因此,只有闭包可以对私有变量进行访问。

//f1是一个闭包
var f1 = foo();
f1();//输出0;
f1();//输出1;
f1();//输出2;
//f2是另外一个闭包,console.log(f1===f2) 输出为false;
var f2 = foo();
f1();//输出0;
f1();//输出1;
f1();//输出2;

从例子可以看到,f1 f2各有各自的私有变量private,两者并不影响。那么闭包是怎么样实现”私有”的呢?


闭包的原理

上文说到JavaScript通过函数作用域去模拟”私有”的,那么我们要从作用域链的创建说起。

  1. foo函数第一次被调用时,会创建一个执行环境(execution context)以及相应的作用域链,并把作用域链赋值给一个特殊的内部属性[Scope]。
  2. 使用this,arguments和其他命名参数初始化函数活动对象(activation object)。作用域链从本函数延伸到全局函数。大概就是这样:foo创建过程
  3. 然后执行到闭包时,同样地进行1.2的步骤,结果就是闭包将外部函数(foo)的活动对象和全局变量对象添加到它的作用域链上闭包创建过程也正是因为闭包对外部函数的变量进行引用,所以无论外部函数是否存在,外部函数的活动对象都会保存在闭包的作用域链上!在闭包的生存期间,该变量并不会被垃圾回收例程回收。

结论

总的来说,闭包就是通过作用域链的特性,将外部函数活动变量保存在自身的作用域链上。
一:通过外部函数的作用域隔离访问,让除闭包外的其他函数无法访问该变量;
二:保持对活动变量的引用来实现私有

实现一个简单的$

实现一个简单的Query

标签(空格分隔): JS JQ


前段时间写了一个简单的JQuery$函数,当然是在不使用querySelector的前提下。感觉自己对正则表达式书写,还有dom节点的遍历有了基础的认识。废话不多说,先说一下实现的功能:

// 可以通过id获取DOM对象,通过#标示,例如
$(“#adom”); // 返回id为adom的DOM对象

// 可以通过tagName获取DOM对象,例如
$(“a”); // 返回第一个对象

// 可以通过样式名称获取DOM对象,例如
$(“.classa”); // 返回样式定义包含classa的对象

// 可以通过attribute匹配获取DOM对象,例如
$(“[data-log]”); // 返回包含属性data-log的对象

$(“[data-time=2015]”); // 返回第一个包含属性data-time且值为2015的对象

// 可以通过简单的组合提高查询便利性,例如
$(“#adom .classa”); // 返回id为adom的DOM所包含的所有子节点中,第一个样式定义包含classa的对象

因为有复杂的组合查询,所以解决思路是确定要查询的对象是简单查询还是组合查询。
利用/\b /.test(queryString)去判断查询语句是否有空格
准确来说,是判断一个单词后是否有空格。 例如:“#id .class”,会匹配到#id后的空格,而对于“ .class”则不能匹配到。

当为简单查询时

查询规则只有一条。但是却分为:
查询id对象(规则为#+“idName”),
查询class对象(规则为.+“className”),
查询特定属性对象(规则为[attrName]),
查询特定属性值对象(规则为[arrtName=arrtValue]),
查询tag对象(排除以上规则,剩下的就是查询tag对象)。
因为规则匹配符不同,所以使用switch(str.charAt(0)),根据匹配规则字符串的第一个字符做判断。

####1.查询id对象。直接利用JS的id选择器。因为id具有唯一性,所以并不需要遍历dom树。####

case "#": // 去除id前的'#'字符,获取id值 name = str.replace(/^#/,""); return document.getElementById(name);

####2.查询class对象。####
因为class的不唯一性,所以我们要去遍历dom树,把所有符合规则的dom节点都找到,放进一个数组。这里遍历dom使用的方法是利用js的createNodeIterator()创建一个dom迭代工具遍历dom树。
document.createNodeIterator(root, whatToShow, filter)

接受的参数有三个:
root:
    遍历树的根节点,即迭代器遍历的起点。
whatToShow:
    迭代器遍历的节点类型,默认是遍历所有节点。可以设置为`NodeFilter.SHOW_ELEMENT`,表示只遍历element节点。
filter:
    节点过滤规则,过滤条件?NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
    然后把符合过滤规则的节点放进一个数组,再返回该数组 

代码:case ".": name = rules[i].replace(/^\./,""); var Iterator = document.createNodeIterator(parentNodeList[c],NodeFilter.SHOW_ELEMENT, function(node){ return new RegExp("^"+name+"$").test(node.className)?NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT; },false); var currentNode; while(currentNode = Iterator.nextNode()) { temp.push(currentNode); } break;

####3.查询特定属性对象。####
对于属性对象,分为只查找属性对象和查找特定属性值对象,用正则表达式检验查询字符串里是否有“=”,有“=”就在dom迭代器的filter里设置过滤规则为:
node.getAttribute(attr)==value
如果没有,则只需要判断是否含有该属性,过滤规则filter设置为:
node.hasAttribute(name)
代码:略

####4.tag对象。####
对于tag对象,由于是不是复数查询,只需要使用
document.getElementsByTagName(str);
就足够了

当为复杂查询时

    当查询条件为复数条件时,先找出符合最外层查询规则的dom节点,放进parentNodeList数组,以该数组内的节点为遍历起点,搜寻符合第二条查询规则的所有dom节点,清空原parentNodeList数组,将符合第二条查询规则的所有dom节点放进parentNodeList数组,以该数组内的节点为遍历起点,搜寻符合第三条查询规则的所有dom节点...直到查询到符合所有规则的节点
    例如:$("#id .class i")
规则一为"#":
先找出id为id的节点,将该节点放进parentNodeList:[div.id]
规则二为".":
以div.id为起点,搜索它的后代节点,后代节点中类名为class的将被放进parentNodeList:[div.class,li.class,span.class]
规则三为tag:
分别以div.class,li.class,span.class为起点,搜索它的后代节点,后代中tag名为i的节点放进parentNodeList[li.class i,span.class i]
后面没有规则了,那么目前的parentNodeList就是我们要寻找的节点。返回parentNodeList。完成查询。
注意事项:
由于id选择器的唯一性,当有$(".class #id")这查询条件时(它的确很无聊而且没意义,可是可能有人就愿意这么查)。
    我们以所有的.class节点为起点去查询#id节点,那么每个#id节点就被重复查询了,到时候返回的parentNodeList可能就是[div.id,div.id,div.id](重复的数目是看.class的节点数)。因为重复了很多次,所以要进行一次去重处理。

由于代码太长,放在demo里面了
query代码和demo

JS深度克隆及类型判断

ife 递归实现深度克隆(内含JS数据类型判断,对象遍历)

今天在做2015ife的题时,感觉收获很多,对于js基本类型有了新的认识。把在研究过程中所得记录下来!
题目是:

// 使用递归来实现一个深度克隆,可以复制一个目标对象,返回一个完整拷贝
// 被复制的对象类型会被限制为数字、字符串、布尔、日期、数组、Object对象。不会包含函数、正则对象等

首先要去判断要克隆的对象的值类型或者引用类型。判断方法有很多种!
对于值类型或者引用有4种方法判断

1.typeof

但是!js的数值有两种构造方法:直接赋值法和通过值函数构造器构造
例如:
var test1 = "string"; var test2 = new String("string2"); console.log(typeof test1);//输出string console.log(typeof test2);//输出object
对于typeof来说;所有通过构造器constructor产生的变量都是object.那么我们怎么去判断用constructor产生的变量?

2.instanceof

instanceof 函数可以判断左边参数是否是右边参数的一个实例!
例如:
console.log(test2 instanceof String);//输出true
但是!
console.log(test1 instanceof String);//输出false
这就很不和谐了!有没有两种都能判断的方法呢?

3.Object.prototype.toString.call()

当然有!MDN在官方教程上就介绍了一种可以判断所有类型的方法!
使用toString()方法来检测对象类型

console.log(Object.prototype.toString.call(test1))//输出[object String] console.log(Object.prototype.toString.call(test1))//输出[object String]
顿时觉得人生豁然开朗了起来!啊~五环!你比四环多一环!

4.Object.constructor

在研究MDN 的api文档的时候,发现了constructor方法!However这个方法
返回一个指向创建了该对象原型的函数引用。需要注意的是,该属性的值是那个函数本身,
而不是一个包含函数名称的字符串!
而不是一个包含函数名称的字符串!!
而不是一个包含函数名称的字符串!!!
对于原始值(如1,true 或 “test”),该属性为只读。
所有对象都会从它的原型上继承一个 constructor 属性.
所以,虽然可以实现判断,但是还是不用为好!
console.log(test1.constructor.toString()=="function String() { [native code] }")//true; console.log(test2.constructor.toString()=="function String() { [native code] }")//true;
总结:方法34对于所有值类型和引用类型适用(推荐第三种方法,毕竟官方),第12种方法看情况使用!

然后来到遍历问题了!
1.字符串的遍历

`
     var temp = src.split("");
        var cloneString="";
        for(var i=0;i<temp.length;i++)
        {
             cloneString+=temp[i];
             }

`
原理就是利用split方法将字符串里一个个字母分开(注意里面的参数为空"",为其他就会以这个参数为标准分离字符串)

2.数组的遍历
使用的是传统的数组遍历

var temp = new Array();
    for(var i=0,a;a = array[i];i++)
    {
         temp[i] = cloneObject(a);
    }

这里遇到了几个坑,先说一下:
当使用
for(var a in array)
{console.log(a+typeof a)}
得到的值是
0 string
1 string
2 string
这种方法并不行
还有一个坑就是当使用
for(var i=0,a;a = array[i++];)
i会在a被赋值后就自动增加而不是等到一个循环完成再增加
;也就是遍历结果是对的,但是i的数值变化是从1开始而不是从0开始的!
在赋值的过程中,我首先使用的是temp.push()方法!但是!push方法会让
temp数组新增加的元素的类型为undefined!这不是我想要的结果!我要的是完美克隆,即数组里面对象类型也要和原来的一致。看了MDN的api接口发现解决方法如下:
Array.prototype.push.apply(temp,array);
3.对象的遍历

var temp = {}; 
           var keys = Object.keys(src);
           // keys 为对象src的键名字数组
           // 它是数组!!!
           for(var i=0,a;a=keys[i];i++)
           {
               temp[a] = cloneObject(src[a]);
           }

对象的遍历,首先获得它的键数组(对象自带的keys()方法),然后再通过键遍历一次值就行了,很简单。

心得:ife的题真的很能锻炼基础。感谢百度前端技术学院!(这波广告我给自己88分)!多看MDN总会有收获!下面附上我的代码!(请无视里面的吐槽注释还有一些小白的地方)

var cloneObject = function(src){
    var Result;
    switch(Object.prototype.toString.call(src)){
        case "[object Number]": 
            Result = (typeof src === "object"?new Number(src):parseInt(src.toString()));
            break;
        case "[object String]":
            // 遍历字符串 =.= 好像没啥意义
            // {
            //     var temp = src.split("");
            //     var cloneString="";
            //     for(var i=0;i<temp.length;i++)
            //     {
            //         cloneString+=temp[i];
            //     }
            // }
            Result = (typeof src === "object"?new String(src):src.toString());
            break;
        case "[object Boolean]":
            Result = (typeof src === "Boolean"?new Boolean(src):src);
            break;
        case "[object Date]":
            Result = new Date(src);
            break;
        case "[object Array]":
            var temp = new Array();
              // Array.prototype.push.apply(temp,src);
             // 当使用for(var i=0,a;a = src[i++];) i会在a被赋值后就自动增加而不是
             // 等到一个循环完成再增加
            for(var i=0,a;a = src[i];i++)
            {
                  // temp.push(cloneObject(a));
                  // 使用push方法会让数组所有元素的类型变成undfined
                 temp[i] = cloneObject(a);
            }
            Result = temp;
            delete temp;
            break;
        case "[object Object]":
            var temp = {}; 
            var keys = Object.keys(src);
            // keys 为对象src的键名字数组
            // 它是数组!!!
            for(var i=0,a;a=keys[i];i++)
            {
                temp[a] = cloneObject(src[a]);
            }
            Result = temp;
            delete temp;
            delete keys;
            break;
        default:
            break;
    }
    return Result;
}