javascript中的一些核心知识点以及须要注意的地方

前言

近期琐事甚多,这些事情的积累对知识体系的提高有好处,可是却不能整理出来,也整理不出来javascript

好比说我最近研究的Hybrid在线联调方案便过于依赖于业务,就算分享也不会有人读懂,如果抽一点来分享又意义不大css

又拿最近作webapp view 转场动画研究,就是几个demo不断测试,感受没有什么可说的html

最后甚至对webapp中的History的处理方案也是有一些心得,一点方案,可是依旧难以整理成文,因而便开始文荒了前端

这个时候不妨便温故知新吧,对javascript的一些老知识点进行整理回顾,以后有大动做再说吧!java

文中知识仅是我的积累总结,有误请指出web

闭包

做用域链

闭包是javascript中一个重要知识点,也是javascript中一块魔法,咱们在不熟悉他的状况下可能便常用了,熟悉他了解他是从初级至中级的一个标志windows

要真正了解闭包,就得从做用域链提及数组

javascript中,做用域链的做用是控制变量的访问顺序,仅此而已

首先,javascript在运行时须要一个环境,这个环境即是咱们所谓执行上下文(execution context浏览器

执行上下文决定了变量或者函数有权利访问其它数据,每一个执行环境都有一个与之关联的变量对象,用于存储执行上下文中定义的变量或者函数闭包

通常状况下咱们所处的全局执行上下文即是window对象,因此全局范围内建立的全部对象所有是window的属性或者方法

函数的变量对象通常是其活动对象(activation Object)

其次,javascript没有块级做用域的概念,可是每一个函数有本身的执行上下文,这个即是变相的块级做用域

每执行一个函数时,函数的执行上下文会被推入一个上下文栈中,函数若执行结束,这个上下文栈便会被弹出,控制权变回以前的执行上下文

当代码在执行上下文中执行时,变回建立一个做用域链,这个做用域链控制着执行上下文数据访问顺序

function test() {
  var a = 2;
  console.log(a);
}
var a = 1;
test();

在这里便具备两个执行上下文,一个是window,一个是test函数

首先,在test执行以前,咱们全局执行上下文已经存在,他即是window,这个时候咱们会有a与test在做用域最前端

执行test时候,造成test执行上下文,因而最前端的执行上下文变成了test,这个时候会先造成活动对象,包括arguments以及a

在console.log时,会访问做用域链最近的a变量,也就是2,这个是打印出2的根本缘由,如果没有做用域链这个顺序就坏了

下面是test执行时候的图示:

因此做用域链相关的知识点是:

① 控制变量访问顺序
② 执行上下文包含一个做用域链的指针
③ 该层函数外部有几个函数,便会有几个活动对象待处理,做用域链指针会指向其外部活动对象
④ 做用域链为执行上下文时函数内部属性,不要妄想去操做

闭包的造成

闭包的造成即是一个函数执行上下文中有一个变量被其内部函数使用了,而且这个内部函数被返回了,便造成了一个闭包

因为函数调用后,外部临时变量保存着内部的引用,执行时会造成内部上下文环境,内部的函数会包含外部的做用域链指向的变量对象,

这个时候就算外部执行环境消耗,因为外部保存着外部函数的活动对象的引用,因此这个变量对象不会被消耗,这个是闭包产生的缘由

function test() {
  var a = 2;
  return function () {
    console.log(a);
  };
}
var b = test();
b();

这里会造成三个执行环境,一个是全局的,一个是test的,一个是匿名函数(最终是b函数)的,咱们依旧从test执行时提及

当test函数执行时:

var b = test();

会造成一个执行上下文,执行上下文包含一个做用域链指针,而且会造成一个活动对象

这里test的做用域链只是一个指针,他只是引用这个活动对象,执行结束后执行上下文会被释放,做用域链也会消失,可是其活动对象未必会GC

在b执行时,其匿名函数的做用域链便指向了外部函数的活动对象,不要问他怎么得到这个指针引用的,他就是知道,因而test的活动对象将一直被保存,直到b调用结束

这里b执行的关系是:

经典例子

关于闭包有一个经典的例子,他即是for循环的例子:

function createFn() {
  var ret = [], i;
  for (i = 0; i < 10; i++) {
    ret[i] = function () {
      return i;
    };
  }
  return ret;
}
var fns = createFn();

这段代码很是简单,根据一个数组造成10个函数,每一个函数返回其索引值,这类应用在实际工做中会常常用到,只不过咱们须要的是其索引对应的数据,而不是简单的索引了

这类会createFn执行时会有两个执行环境,一个是本身的,一个是windows的,内部执行环境做用域链会指向一个活动对象

固然fns数组中任意一个函数执行时,其会使用到createFn的活动对象中的数据i,而该活动对象是被10个函数共用的,都是10,因此与预期不合

该问题的处理即是各自造成本身的闭包:

function createFn() {
  var ret = [], i;
  for (i = 0; i < 10; i++) {
    ret[i] = (function (i) {
      return function () {
        return i;
      };
    })(i);
  }
  return ret;
}
var fns = createFn();

这里循环中会造成10个独立的执行上下文,其中的10个活动对象的arguments都保存了外部i的独立数据,而内部又造成一个闭包访问当即执行函数的数据,因此数据正确了......

其它闭包

requireJS中的闭包

标准的requireJS来讲都是一个AMD的模块,好比:

define(function () {
  var add = function (x, y) {
    return x + y;
  };
  return {
    add: add
  };
});

咱们知道,requireJS每一次加载其模块皆会被执行一次,而且只会执行一次,这个模块会被requireJS所保存,因此这个匿名函数活动对象是不会被释放的,且是惟一的

这个时候咱们不少组件即可以统一使用其功能便可,好比生成uuid什么的......固然,这种不释放的问题,也会致使heap值的提高,这个是否是有问题便须要各位去验证了

webapp中的闭包

webapp通常会使用requireJS管理模块,而内部又会造成许多view的实例,这个实例而且会保存下来,这样也会致使不少函数的活动对象得不到释放

一来二往之间,heap值会比传统网站高,这个是webapp一块比较头疼的地方,须要慢慢优化

原型链

最初javascript没有class的概念,咱们使用的类是以function模拟,继承的实现手段通常依靠原型链,继承的使用也是评价一个jser的重要指标

每一个函数都会包含一个原型对象prototype

原型对象prototype包含一个指向构造函数的指针constructor

实例对象包含一个内部属性__proto__指针指向原型对象prototype

这是他们之间的三角关系:

(function () {
    var Person = function (name) {
        this.name = name;
    };
    //Person.prototype = {};//这句将影响十分具备constructor属性
    Person.prototype.getName = function () {
        return this.name;
    };

    var Student = function (name, sex, id) {
        this.name = name || '无名氏';
        this.sex = sex || '不明';
        this.id = id || '未填'; //学号
    };
    //至关于将其prototype复制了一次,如果包含constructor的话将指向Person
    Student.prototype = new Person();
    Student.prototype.getId = function () {
        return this.id;
    }
    var y = new Person();
    var s = new Student;
    var s1 = y instanceof Person;
    var s2 = s instanceof Student;
    var s3 = s instanceof Person;
    var s4 = Student.prototype.constructor === Person;
    var s5 = Student.constructor === Person;
    var s6 = Student.constructor === Function;

    var s = '';
})();

通常形式的继承方式如上,偶尔咱们会这样干:

Student.prototype = {}

可是这样会致使prototype对象的constructor对象丢失,因此须要找回来,另一个问题是,这里继承须要执行父类的构造方法,这样是有问题的

好比,父类的构造函数中有一些事件绑定什么的与子类无关,便会致使该类继承无用,因此不少时候咱们须要本身实现继承,比较优雅的是prototype的作法,我这里对其进行了必定改造

var arr = [];
var slice = arr.slice;

function create() {
  if (arguments.length == 0 || arguments.length > 2) throw '参数错误';

  var parent = null;
  //将参数转换为数组
  var properties = slice.call(arguments);

  //若是第一个参数为类(function),那么就将之取出
  if (typeof properties[0] === 'function')
    parent = properties.shift();
  properties = properties[0];

  function klass() {
    this.initialize.apply(this, arguments);
  }

  klass.superclass = parent;
  klass.subclasses = [];

  if (parent) {
    var subclass = function () { };
    subclass.prototype = parent.prototype;
    klass.prototype = new subclass;
    parent.subclasses.push(klass);
  }

  var ancestor = klass.superclass && klass.superclass.prototype;
  for (var k in properties) {
    var value = properties[k];

    //知足条件就重写
    if (ancestor && typeof value == 'function') {
      var argslist = /^\s*function\s*\(([^\(\)]*?)\)\s*?\{/i.exec(value.toString())[1].replace(/\s/i, '').split(',');
      //只有在第一个参数为$super状况下才须要处理(是否具备重复方法须要用户本身决定)
      if (argslist[0] === '$super' && ancestor[k]) {
        value = (function (methodName, fn) {
          return function () {
            var scope = this;
            var args = [function () {
              return ancestor[methodName].apply(scope, arguments);
            } ];
            return fn.apply(this, args.concat(slice.call(arguments)));
          };
        })(k, value);
      }
    }

    klass.prototype[k] = value;
  }

  if (!klass.prototype.initialize)
    klass.prototype.initialize = function () { };

  klass.prototype.constructor = klass;

  return klass;
}
View Code

首先,继承时使用一个空构造函数实现,这样不会执行原构造函数的实例方法,再规范化必须实现initialize方法,保留构造函数的入口,这类实现比较优雅,建议各位试试

javascript中的DOM事件

事件流

PS:javascript的事件一块我说的够多了,这里再说一次吧......

javascript注册dom事件的手段不少:

① 直接写在dom标签上,onclick的作法

② 在js中这样写:el.onclick = function

上述作法事实上是很差的,由于他们没法屡次定义,也没法注销,更加不用说使用事件委托机制了

上述两种作法的最终仍然是调用addEventListener方式进行注册冒泡级别的事件,因而这里又扯到了javascript事件的几个阶段

在DOM2级事件定义中规定事件包括三个阶段,这个是现有DOM事件的基础,这个一旦改变,前端DOM事件便须要重组
三个阶段是事件事件捕获阶段、处于目标阶段、冒泡阶段
事件捕获由最早接收到事件的元素往最里面传
事件冒泡由最具体元素往上传至document

通常而言是先捕获后冒泡,可是处于阶段的事件执行只与注册顺序有关,好比:
每次点击一个DOM时候咱们会先判断是否处于事件阶段,如果到了处于阶段的话便不存在捕获阶段了
直接按照这个DOM的事件注册顺序执行,而后直接进入冒泡阶段逻辑,其判断的依旧是e.target与e.currentTarget是否相等

这个涉及到一个浏览器内建事件对象,咱们注册事件方式多种多样
除了addEventListener能够注册捕获阶段事件外,其他方式皆是最后调用addEventListener接口注册冒泡级别事件
注册的事件队列会根据DOM树所处位置进行排列,最早的是body,到最具体的元素
每次咱们点击页面一个区域便会先作判断,是否处于当前阶段,好比:
我当前就是点击的是一个div,若是e.target==e.currentTarget,这个时候便会按注册顺序执行其事件,不会理会事件是捕获仍是冒泡,而跳过捕获流程,结束后会执行冒泡级别的事件,如果body上有冒泡点击事件(没有捕获)也会触发,以上即是DOM事件相关知识点

事件冒泡是事件委托实现的基石,咱们在页面的每次点击最终都会冒泡到其父元素,因此咱们在document处能够捕捉到全部的事件,事件委托实现的核心知识点是解决如下问题:

① 咱们事件是绑定到document上面,那么我怎么知道我如今是点击的什么元素呢

② 就算我能根据e.target获取当前点击元素,可是我怎么知道是哪一个元素具备事件呢

③ 就算我能根据selector肯定当前点击的哪一个元素须要执行事件,可是我怎么找获得是哪一个事件呢

若是能解决以上问题的话,咱们后面的流程就比较简单了

肯定当前元素使用 e.target便可,因此咱们问题以解决,其次便根据该节点搜索其父节点便可,发现父节点与传入的选择器有关便执行事件回调便可

这里还须要从新e.currentTarget,不重写所有会绑定至document,简单实现:

var arr = [];
var slice = arr.slice;
var extend = function (src, obj) {
  var o = {};
  for (var k in src) {
    o[k] = src[k];
  }
  for (var k in obj) {
    o[k] = obj[k];
  }
  return o;
};

function delegate(selector, type, fn) {
  var callback = fn;

  var handler = function (e) {
    //选择器找到的元素
    var selectorEl = document.querySelector(selector);
    //当前点击元素
    var el = e.target;
    //肯定选择器找到的元素是否包含当前点击元素,若是包含就应该触发事件
    /*************
    注意,此处只是简单实现,实际应用会有许多判断
    *************/
    if (selectorEl.contains(el)) {
      var evt = extend(e, { currentTarget: selectorEl });
      evt = [evt].concat(slice.call(arguments, 1));
      callback.apply(selectorEl, evt);
      var s = '';
    }
    var s = '';
  };

  document.addEventListener(type, handler, false);
}
View Code

事件委托因为所有事件是绑定到document上的,因此会致使阻止冒泡失效,不少初学的同窗不知道,这里要注意

事件模拟

事件模拟是dom事件的一种高级应用,通常状况下用不到,可是一些极端状况下他是解决实际问题的杀手锏

事件模拟是javascript事件机制中至关有用的功能,理解事件模拟与善用事件模拟是判别一个前端的重要依据,因此各位必定要深刻理解

事件通常是由用户操做触发,其实javascript也是能够触发的,比较重要的是,javascript模拟的触发遵循事件流机制!!!

意思就是,javascript触发的事件与浏览器自己触发实际上是同样的,简单模拟事件点击:

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
    <style type="text/css">
         #p { width: 300px; height: 300px; padding: 10px;  border: 1px solid black; }
         #c { width: 100px; height: 100px; border: 1px solid red; }
    </style>
</head>
<body>
    <div id="p">
        parent
        <div id="c">
            child
        </div>
    </div>
    <script type="text/javascript">
        alert = function (msg) {
            console.log(msg);
        }

        var p = document.getElementById('p'),
        c = document.getElementById('c');
        c.addEventListener('click', function (e) {
            console.log(e);
            alert('子节点捕获')
        }, true);
        c.addEventListener('click', function (e) {
            console.log(e);
            alert('子节点冒泡')
        }, false);

        p.addEventListener('click', function (e) {
            console.log(e);
            alert('父节点捕获')
        }, true);

        p.addEventListener('click', function (e) {
            console.log(e);
            alert('父节点冒泡')
        }, false);

        document.addEventListener('keydown', function (e) {
            if (e.keyCode == '32') {
                var type = 'click'; //要触发的事件类型
                var bubbles = true; //事件是否能够冒泡
                var cancelable = true; //事件是否能够阻止浏览器默认事件
                var view = document.defaultView; //与事件关联的视图,该属性默认便可,无论
                var detail = 0;
                var screenX = 0;
                var screenY = 0;
                var clientX = 0;
                var clientY = 0;
                var ctrlKey = false; //是否按下ctrl
                var altKey = false; //是否按下alt
                var shiftKey = false;
                var metaKey = false;
                var button = 0; //表示按下哪个鼠标键
                var relatedTarget = 0; //模拟mousemove或者out时候用到,与事件相关的对象
                var event = document.createEvent('Events');
                event.myFlag = '叶小钗';
                event.initEvent(type, bubbles, cancelable, view, detail, screenX, screenY, clientX, clientY,
ctrlKey, altKey, shiftKey, metaKey, button, relatedTarget);
                
                console.log(event);
                c.dispatchEvent(event);
            }
        }, false);
    </script>
</body>
</html>
View Code

模拟点击事件是解决移动端点击响应的基石,有兴趣的同窗本身去研究下吧,我这里很少说

延时执行

延时执行settimeout是javascript中的一道利器,不少时候一旦解决不了咱们便会使用settimeout,可是对settimeout的理解上,不少初学的朋友有必定误区

初学的朋友通常认为settimeout是在多少毫秒后便会被执行,事实上其后面的数据表明的是一个时间片,或者说是优先级,settimeout的回调会在主干程序以后执行

好比:

var a = 0, b = 1;
setInterval(function () {
  a = 1;
}, 0)
while (1) {
  //...
  b++;
  if(a == 1)
    break;
}

如下代码会致使浏览器假死,由于settimeout中的代码永远不会执行

settimeout真正的的用法是:

① 延时请求,减小没必要要的请求

② 须要过多的操做dom结构时,为了闭包浏览器假死,可使用settimeout

另外,zepto中有一段与settimeout有关的耻辱代码,在模拟tap事件时候,zepto使用dom模拟click事件的方式实现了:

.on('touchend MSPointerUp pointerup', function(e){
  if((_isPointerType = isPointerEventType(e, 'up')) &&
    !isPrimaryTouch(e)) return
  cancelLongTap()

  // swipe
  if ((touch.x2 && Math.abs(touch.x1 - touch.x2) > 30) ||
      (touch.y2 && Math.abs(touch.y1 - touch.y2) > 30))

    swipeTimeout = setTimeout(function() {
      touch.el.trigger('swipe')
      touch.el.trigger('swipe' + (swipeDirection(touch.x1, touch.x2, touch.y1, touch.y2)))
      touch = {}
    }, 0)

  // normal tap
  else if ('last' in touch)
    if (deltaX < 30 && deltaY < 30) {
      tapTimeout = setTimeout(function() {

        var event = $.Event('tap')
        event.cancelTouch = cancelAll
        touch.el.trigger(event)

        if (touch.isDoubleTap) {
          if (touch.el) touch.el.trigger('doubleTap')
          touch = {}
        }
        else {
          touchTimeout = setTimeout(function(){
            touchTimeout = null
            if (touch.el) touch.el.trigger('singleTap')
            touch = {}
          }, 250)
        }
      }, 0)
    } else {
      touch = {}
    }
    deltaX = deltaY = 0
})
View Code

比较狗血的是,他在tap这里使用了settimeout,致使了一个延时,这个延时效果直接的影响即是其event参数失效

也就是这里,touchend时候传入的event参数不会被tap事件用到,什么e.preventDefault之类的操做便于tap无关了,此类实现至今未改

其它

localstorage

localstorage的使用在我厂webapp的应用中,达到了一个史无前例的高度,咱们惊奇的发现,其真实容量是:

localstorage 的最大限制按字符数来算,中英文都是最多500多万个字符,webkit为5242880个

因而不少时候,localstorage的滥用便会引起localstorage存储失效,致使业务错误

而且localstorage的滥用还表如今存储业务关键信息致使url对外不可用的状况,因此使用localstorage的朋友要慎重!

其它

......

结语

今天咱们花了一点时间回顾了一些javascript的核心知识点,但愿对各位有用,我这里先撤退了,文中理解有误请提出

相关文章
相关标签/搜索