JavaScript Common Codes & Snippets

本文主要收集平常使用到的一些JavaScript Codes和Snippets。


通过闭包构造私有成员

function foo() {
  var id = 0;
  return function () {
    return id++;
  }
}

var makeid = foo();

var i = makeid();
var j = makeid();

在函数foo中,id实际上是私有成员,我们通过且仅能通过闭包函数makeid()访问它。

私有成员:私有Private成员要由构造器生成。构造器中的普通的var变量和参数都成为私有成员。

function Container(param) {
  this.member = param;
  var secret = 3;
  var self = this;
}

这个构造器有三个私有实例变量:param, secret, 和self,它们被附加到了对象上,但它们无法从外部访问,同时它们也无法被这个对象的公共方法所访问。他们只对私有方法和特权方法可见。私有方法则是构造器内部的函数,而特权方法是用this在构造器中分配的。

如果想了解更多关于私有成员的实现请参考Private properties in JavaScript.

使用闭包保存变量所在的作用域

function foo() {
  var req = new XMLHttpRequest();
  req.onreadystatechange = function () { /*此处创建闭包*/
    if (req.readyState == 4) {
      ...
    }
  }
}

尽管onreadystatechangereq的属性或是方法,但是它并不是由req触发的。因为它需要访问它自己,所以解决方案可以是使用一个 全局变量(如window.req)或是使用闭包让这些变量的值始终保存在内存中,让它不至于在foo()函数执行完毕就被GC回收。

getClickHandler: function () {
  var me = this;
  return function(e) {
    me.showTip();
    ...
  }
}

以上编程模式,是经常会使用到的其中一种闭包写法。

闭包与变量(闭包所能访问的变量,它是可以被改变的)

闭包是指在建立函数时,绑定了当时作用域下的有效的自由变量。基于此,首先我们给闭包下个定义:

闭包就是能够读取其他函数内部变量的函数。

闭包最大用处有两个:

那么什么是自由变量呢?

自由变量是相对于函数而言,既不是本地变量也不是参数的变量,它的作用范围基本上是在函数定义的范围中。举个例子:

function Robot() {
  var createTime = new Date();

  this.getAge = function () { // 这里建立了Closure
    var now = new Date();
    var age = now - createTime;
    return age;
  }
}

var robot = new Robot();
alert(robot.getAge());

单单看getAge函数的话,createTime并没有多大的意义,对getAge而言,createTime是一个自由变量,同时它也是Robot函数的本地私有成员变量,但是在getAge所引用的函数中被关闭了,从而createTime的生命周期得以延续,即使它是使用var声明的,只要getAge中所引用的Function对象还存在,createTime自由变量也就能继续存活。

关于闭包,Crockford曾经说过:

一个内部的函数总是可以访问这个函数外部的变量和参数,甚至在外部的函数返回之后。

var myAlerts = [];

for (var i = 0; i < 2; i++) {
  myAlerts.push(
    function inner() {
      alert(i);
    });
}

myAlerts[0](); // 2
myAlerts[1](); // 2

第一眼看过去,JavaScript新手会认为alert(i)会弹出inner函数定义时逐步增长的i值,也就是分别显示01。这是我们最常犯的一种错误。inner函数是在全局上下文中创建的,从而它的scope chain是静态地绑定到全局上下的。当调用inner函数时,它会去找寻标识符i,而标识符的搜寻是沿着scope chain来的。所以它会去innerscope chain上搜救变量i,但是,在调用inner函数的时候,变量i的值已经变为2,所以每次调用inner函数的结果都会是一样的。

JavaScript的一个很重要的特性就是它的解析器使用的是Lexical Scoping,意味着所以内部函数都是静态地绑定到创建它们的父上下文中的。闭包只能取得包含函数中任何变量的最后一个值。也就是说,闭包所保存的是整个变量对象,而不是某个特殊的变量。

上面的例子中,所有的myAlerts中保存的函数中能会引用到2,也就是被关闭的变量i的最后一个值(循环的计数器i在循环中的内部函数中被关闭了,它的生命周期得以延续,即使它是使用var声明的,只要myAlerts中所引用的Function对象还存在,i也就能继续存活)。

但是我们通过使用闭包保存变量所在的作用域解决。

var myAlerts = [];

for (var i = 0; i < 2; i++) {
  myAlerts.push(
    (function inner(i) {
      return function () {
        alert(i);
      }
    })(i));
}

myAlerts[0](); // 0
myAlerts[1](); // 1

创建对象的各种方法(八仙过海,各显神通)

自定义构造器函数

var obj = new function() {
  /* stuff */
};
  

对象字面量

var obj = {
  /* stuff */
};

Object.create

Object.createECMAScript 5引入的新方法,它是new的一个变体,它可以让你基于一个原型对象来创建一个新的对象。

var prototypeDef = {
  protoBar: "protoBar",
  protoLog: function () {
    console.log(this.protoBar);
  }
};

var propertiesDef = {
  instanceBar: {
    value: "instanceBar"
  },
  instanceLog: {
    value: function () {
      console.log(this.instanceBar);
    }
  }
}

var foo = Object.create(prototypeDef, propertiesDef);
foo.protoLog(); // logs "protoBar" 
foo.instanceLog(); //logs "instanceBar"

这里有几点需要注意:

我们可以解决第二个问题,首先初始化propertyArrayValue_null,然后当你添加元素时才初始化该数组。

var prototypeDef = {
  protoArray: [],
};

var propertiesDef = {
  propertyArrayValue_: {
    value: null,
    writable: true
  },
  propertyArray: {
    get: function () {
      if (!this.propertyArrayValue_) {
        this.propertyArrayValue_ = [];
      }
      return this.propertyArrayValue_;
    }
  }
};

var foo = Object.create(prototypeDef, propertiesDef);
var bar = Object.create(prototypeDef, propertiesDef);
foo.protoArray.push("foobar");
console.log(bar.protoArray); //logs ["foobar"] 
foo.propertyArray.push("foobar"); 
console.log(bar.propertyArray); //logs []

定义对象方法

function MyObj() {}

MyObj.prototype = {
  foo: function () {
      alert(this);
  },
    ...etc..
};

var my1 = new MyObj();
my1.foo();

改变目标元素的显示/隐藏状态

T.dom.toggle = function (element) {
  element = T.dom.g(element);
  element.style.display = element.style.display == "none" ? "" : "none";

  return element;
};

使用display:'',与之相对的就是display:block

设置elem.style.display = ''仅仅设置或是移除了内联样式。如果用户自定义的样式存在并且已经应用到了该元素上,那么一旦内联样式被移除了,该元素仍然会使用自定义样式来渲染。(这是因为elem.style.display已经移除了高优先级的内联样式,但是自定义的样式并没有受到影响,仍然会起作用。)因此,

  1. 在通过style.display=...改变目标元素的显示或隐藏状态时,不要同时使用非内联样式和内联样式(只使用内联样式)
  2. 或者,如果是使用className样式或是同时使用内联/非内联样式,可以通过className以及display属性来完成
  3. 又或者,避免使用elem.style.display = ''这种形式,而使用elem.style.display = 'none'|'block'的这种形式

修改目标元素的CSS样式

//inline style
elem.style.xxx = ...
//classname
elem.className  = ...

修改内联样式或是修改className即可。

使用className

var elem = $('#id');
elem.className = 'foo';

使用className,而不是class,那是因为在ECMAScript 5中class是一个Future Reserved Word,而在ECMAScript 6class已经是一个关键词了。

DOM Level 0 中的事件处理模型

事实上,并不存在DOM Level 0标准,它只存在于DOM的历史长河中。DOM Level 0通常被认为是Internet Explorer 4.0 and Netscape Navigator 4.0中所支持的DHTML

DOM Level 0中的事件处理模型由Netscape Navigator引入,有二种主要的类型:

内联模型

在内联事件模型中,事件处理器是作为HTML元素的属性添加的。如下所示:

<p>Hey <a href="http://www.example.com" onclick="triggerAlert('Joe'); return false;">Joe!</p>

如果想了解更多,请参考这里

传统模型

在传统事件模型中,事件处理器是通过JavaScript脚本添加或删除的。与内联模型相同的是,每一个事件一次只能绑定一个事件处理器。

element.onclick = doSomething; //添加事件处理器
element.onclick = null; // 移除事件处理器

如果想了解更多,请参考这里

使用id属性

在使用id属性时,文档中所有的元素必须都有唯一的id。对于多个元素使用相同的id,在IE中会导致各种种样的问题。

动态添加className

var table = document.getElementById('myTable');
var rows = table.getElementsByTagName('tr');
for (var i = 0; i < rows.length; i++) {
  if (i % 2) {
    rows[i].className += " even";
  }
}

在上面的代码中,展示了给一个table添加class以实现斑马线的效果。对于onmouseover/onmouseout的JS事件处理函数也可以通过这种方式添加。

解脱DOM

$('foo').related = $('bar');

给DOM对象添加指向其它相关联的DOM对象的属性,这种方式不会导致IE中的内存溢出,那是因为所有的操作都是在DOM内完成的。

更多请参考IE的Memory Leak

强制浏览器Layout/Reflow

浏览器渲染

首先有个问题:

浏览器是如何渲染一个Web页面的?

从整体上来看,下面是浏览器渲染引擎在取得HTML内容之后的基本流程:

解析HTML以构建DOM树 -> 构建Render树 -> 布局Render树 -> 绘制Render

  1. 首先,基于从服务端返回的HTML字符串构建DOM树
  2. 加载并解析样式(包括外部CSS文件以及内联样式),从而形成CSSOM(CSS Object Model)(CSSOM提供了一套API来操作CSS)
  3. 基于DOM和CSSOM构建别个一棵Render树,它包含一系列需要被渲染的对象(Webkit称这些对象为RendererRender object,而Gecko叫frame),但是Render树并不会包含一些不可见的DOM元素(包含<head>元素或是拥有display:none样式的元素)。每个Renderer都包含与之对应的DOM对象和计算完成的样式信息(如颜色,大小等等),它们将按照正确的顺序显示到屏幕上
  4. 针对每一个Renderer,还需要计算它在屏幕上的确切坐标,这一步叫做Layout
  5. 最后就是Painting,即遍历Render树,并使用UI后端层在浏览器窗口中绘制每个节点

重绘(Repaint/Redraw)

修改元素样式(如background-color, border-color, visibility)时,如果并不影响元素在页面中的位置,那么浏览器只是会使用新的样式信息重绘该元素。

Layout/Reflow

除了重绘外,另外一些改变会影响文档的内容,结构或是元素位置,那么就会发生重绘(Webkit叫Layout,Gecko叫Reflow)。通常如下的一些修改会触发重新定位或回流:

看一个简单来自Google Speedtracer的例子:

// This line invalidates layout.
elementA.className = 'foo';

// This line requires layout to be up to date.
var aWidth = elementA.offsetWidth;

// Invalidates layout again.
elementB.className = 'bar';

// Requires layout to be up to date
var bWidth = elementB.offsetWidth;

通常来说,浏览器会尽可能地把重绘限制在更新的元素的那块区域。比如某个元素的重绘只会影响它后面的元素。 所以,我们可以通过批量读取和批量设置来解决。

// This line invalidates layout.
elementA.className = 'foo';
elementB.className = 'bar';

// This line requires layout to be up to date.
var aWidth = elementA.offsetWidth
// Second Layout pass not needed.
var bWidth = elementB.offsetWidth;

参考:

在JavaScript中获取内联CSS样式

我们可以通过x.style来访问内联样式,如下所示。另外,内联样式会覆盖掉其它的样式,除非使用!important来提高指定样式规则的权重。

element.style.color

color 只在下列两种情况下是有效的:

  1. 它是在element的内联样式定义上设置的
  2. 它已经通过JavaScript中赋过值,如下所示:
element.style.color  'red';

否则,color 都是无效的。

DOM/HTML: 属性那是相当有用的

<a href='javascript:showImg()'>
  <img title='my image' name='sunset'>

比如,在一个简单的slide展示中,鼠标滑过一个链接时,需要根据一些信息在图片显示区域展示相应的一张图片。那么img标签的title和name属性就可以帮你很容易的达到目的。 当然,其它一些自定义的属性也可以使用的。

另外一个例子是:

<input type=text pattern='^\w+$' required=true />

页面加载时,JS代码可以遍历所有的表单字段,并且如果pattern和required属性存在的话,那么就添加一个validate函数,该函数主要负责在表单提交前,根据正则表达式来验证文本框中的内容。

简单的动画

---| 
-----| 
--------| 
-----------|

一个进度条的简单动画实现:我们可以通过逐渐增加divimg的宽度来实现。

function progress() {
  var img = $('img');
  if(img.width < 200) {
    img.width += 5;
    // Don't autosize height along with width
    img.height = 5;
  } else {
    img.width = origwidth;
  }
}

setInterval('progress()', 500);

简单的淡入/淡出效果

淡入/淡出效果一般用于从页面中添加/删除某个元素时。

function fade(el) {
  var b = 155;
  var el = $('div');

  function f() {
    el.style.background = 'rgb(255,255,' + (b += 4) + ')';
    if (b < 255) {
      setTimeout(f, 40);
    }
  };
  f();
}

fade();

更多的可以参考这里

禁止某个资源的Caching

document.write("< img src='foo.jpg?" + Math.random() + "' />"); 

我们可以修改URL,添加了一个伪随机数,这是一种常用的手段,常常用于禁止浏览器缓存图片。

DOM/HTML:常用的方法

更多的信息请参考quirksmode

我们常用的一些JavaScript库,如jQuery, Prototype等等,已经实现了通过CSS2/CSS3选择器返回元素集合,所以除了这些原生的方法外,我们还可以有更多的选择。

节点属性

node.nodeType

`nodeType’包含一个表示元素类型的数字。我们经常用到的如下所示:

  1. Node.ELEMENT_NODE == 1
  2. Node.ATTRIBUTE_NODE == 2
  3. Node.TEXT_NODE == 3
  4. Node.CDATA_SECTION_NODE == 3
  5. Node.ENTITY_NODE == 6
  6. Node.DOCUMENT_NODE == 9
  7. Node.DOCUMENT_FRAGMENT_NODE == 11
node.nodeValue

对文本节点来说,nodeValue表示真正的文本。value of text for a Text node (useful for #text). Null for most other nodes (including element nodes) devedge link

<p>I am a JavaScript hacker.</p> 
var x = [the paragraph];
var text = x.firstChild.nodeValue;

而对于属性节点来说,nodeValue则表示的是属性值。 除此之外,对于document和其它元素节点来说,它会返回null

node.nodeName

nodeName是最有用的一个属性。与它的名字一样,它包含了节点的名字。元素节点的名字始终与标签名相同,属性节点的名字始终与属性名字相同,文本节点的名字始终是#text,而文档节点的名字始终是#document

只是有一点我们需要注意:对于HTML元素节点来说,不管你在HTML中写的是大写或是小写,nodeName都会返回大写的标签名。

node.getAttribute(name)

返回节点上你需要查询的属性的值。

#### node.setAttribute(name, value)

设置节点上某个指定的属性的新的值。

<img src='sample.png' id='test' />
var imgEl = document.getElementById('test'); 
alert(imgEl.getAttribute('src'));
imgEl.setAttribute('src', 'sample.png');

节点上的操作

搜索节点
  1. Children: firstChild, lastChild, childNodes
  2. Siblings: nextSibling, previousSibling
  3. Parents: parentNode
添加/删除 nodes
  1. removeChild(node), appendChild(node)
  2. insertBefore(newNode, referenceNode)
  3. replaceChild(replacingNode, replacedNode)

从DOM树中移除当前的node

var node = ..some_element..
node.parentNode.removeChild(node);

在另外一个节点前插入一个节点

var newItem = document.getElementsByTagName('p')[0]; 
var existingItem = document.getElementsByTagName('h1')[0]; 
newItem.parentNode.insertBefore(newItem, existingItem);

返回值

insertBefore()返回的是被插入节点的一个引用。

var x = y.insertBefore(newItem, existingItem);

那么现在x就包含对newItem的一个引用。

克隆一个节点(浅/深克隆)

var node = ...
var newNode = node.cloneNode(true|false); // true=deep copy, else shallow

需要注意的是,克隆节点时,并不会同时克隆事件处理函数。

使用另外一个节点替换一个子节点

replaceChild()方法可以允许你将一个节点替换为另外一个节点。如果被插入的节点已经在DOM中了,那么它首先会从当前的DOM中位置移除掉。并且插入的节点和被替换的节点都会保持它们的所有子节点不变。

var newNode = document.getElementsByTagName('h1')[0];
var oldNode = document.getElementsByTagName('p')[1];
newNode.parentNode.replaceChild(newNode, oldNode);

返回值

replaceChild()返回的是被替换的节点的一个引用。

var x = y.replaceChild(newNode, oldNode);

那么现在x就包含对oldNode的一个引用。

Related Posts

Xin(Khalil) Zhang 02 February 2015
blog comments powered by Disqus