一些js笔记

本文主要内容是笔者学js以来记录的一些问题以及一些使用技巧。大部分都可以在网路上找到,因为时过久远,不保证所有链接都有效.

生成包含n个元素的数组

1
2
3
4
5
6
7
8
let generateArray = n => Array(n + 1); // 这种网上看到的,比较多了,缺点是不可用map遍历,若非要遍历可以Array(n + 1).join(c).split(c)
let generateArray2 = n => Array.apply(null, { length: n }); // 这种是在Vuejs里看到的,比较实用
let generateArray3 = n => Array(n).fill(undefined);
// 应用:字符串复制
let copyStr = (str, n) => Array(n + 1).join(str);
// 应用: 生成类似range数列
let range = (start, end) => Array.apply(null, { length: end - start + 1}).map(() => start++);

ES6 let变量暂时性死区域DTZ

参见阮一峰的ES6教程

ES6明确规定,如果区块中存在let和const命令,这个区块对于这些命令声明的变量,从一开始就形成封闭作用域。凡是在声明之前使用这些变量就会报错.

JS引擎在使用变量时,他们的得生命周期包含三个部分:声明阶段初始化阶段赋值阶段
比如,以下代码就会报错

1
2
3
let x = "a";
var x = "b"; // redeclared error
let t = t; // undefined error

相关联的有那个变量声明提升以及匿名函数声明提前.
比如:

1
2
3
4
5
6
7
8
var a = 10;
!function () {
console.log(a); // undefined
a = 1;
console.log(a); // 1
var a = 2;
console.log(a); // 2
}();

注意网上关于let/const是否存在hoisting是有争议的,主要争议来源MDN上明确说let不支持hoisting,而Ecmscript里提到过let/const hoisting这个关键词。我个人理解是hoisting只是一个概念,而且本身并没有准确的定义,其实只要知道js引擎在处理var和let/const的区别就可以了,没必要把这个概念强加在let上。

参考文章:【译】JavaScript 变量的生命周期:为什么 let 不存在变量提升

引用传值问题

之前对此我也很疑惑,后来发现只要这样理解就可以了.

1
2
3
4
var b = a = { n: 1 };
a.x = a = { n: 2 };
console.log(a.x); // undefined
console.log(b.x); // { n: 2 }

结果可能跟你想象的不太一样!我的理解是在解释器在进行编译时,执行a.x = a = { n: 2 }时,a均是指向的初始地址{ n: 1 }所处的地址空间,也即b指向的地址.
a = { n: 2 }所做的操作即是a重新指向另外一个存储{ n: 2 }的地址.前面已经说过了,a.x指向的仍旧是原来的地址.最后结果就是:

1
2
3
4
5
6
7
a = { n: 2 }
b = {
n: 1,
x: {
n: 2
}
}

再看一例子说明一下:

1
2
3
4
5
6
7
8
9
10
function f1 (x) {
x = { a: 1 };
}
function f2 (x) {
x.a = 1;
}
var o1 = {}, o2 = {};
f1(o1);
f2(o2);
// 执行完后o1 = {}, o2 = { a: 1 }

可以理解为函数调用过程let tmp = 实参,然后对tmp进行计算

函数式编程

关于函子Functor:
famp: (a->b) -> [a] -> [b]
famp参数(a->b)函数映射,[a]函子值,返回[b]函子值
简单讲就是函数可以作用域函子值上,结果就是该函数在函子上的映射结果
关于函数式不是太理解,可能以后会学习Haskell加深理解

  1. [推荐]参见js函数式编程指南在线阅读
  2. 函数式编程中Functor和Monad的形象解释
  3. Functor, Applicative以及Monad的图片阐释

关于void

1
void expression === undefined; // true

注意expression必须是一个整体,要括起来,和undefined的区别是会计算expression,若你看到有的人代码写void(0)也好,void 0也好,void 666也好,不要惊讶,因为它和undefined等价!

注意:在ES5之前,undefined可以被改写!不过好在ES6之后,undefined只可读,但是局部变量依然可以对之改写!
所以说用void 0来赋值undefined是一个好习惯!

关于const的问题

问题:const修饰的变量可以改吗?

1
2
3
4
5
const a = 12;
a = 0; // assigment to constant variable
const b = {};
b.p = 1; // that's ok

const修饰的变量指向地址不可变,但是里面存储的值是可以变动的!
值得注意的是const修饰的变量需要初始化.

箭头函数的一个作用

箭头函数可以节省代码, => 替代function还是比较简洁的。值得注意的是箭头函数绑定的是父作用域,所以不用担心再es5中this奇怪指向,基本上可以少写一些bind之类的函数.
顺带了解一下ES5里绑定代码块作用域关键字with(虽然跟goto一样因为容易被滥用,所以基本上被禁用的)。但是有些时候还是会有用的,比如vue编译模板就用到了with来绑定模板作用域。所以你在vue组件里prop可以直接写:attr="attr"而不用写成:attr="this.attr"

1
2
3
4
5
var a = 2;
with ({ a: 1 }) {
console.log(this.a); // 2
console.log(a); // 1
}

Object的一个问题

1
2
typeof Object; // function
new Object;

关于原型链

ES5注解里4.2.1节有一段描述Object中prototype:

Every object created by a constructor has an implicit reference (called the object’s prototype) to the value of its constructor’s “prototype” property. Furthermore, a prototype may have a non-null implicit reference to its prototype, and so on; this is called the prototype chain. When a reference is made to a property in an object, that reference is to the property of that name in the first object in the prototype chain that contains a property of that name. In other words, first the object mentioned directly is examined for such a property; if that object contains the named property, that is the property to which the reference refers; if that object does not contain the named property, the prototype for that object is examined next; and so on.

Objects
里面的prototype是显式引用,__proto__是隐式引用(部分浏览器支持__proto__),该图指明了这样一个关系:

1
2
3
4
5
6
7
Constructor.prototype --> constructorPrototype <-- ObjectInst.__proto__
// 即每一个由构造函数生成的object都有一个隐式引用__proto__指向构造函数的原型
function F () {
this.a = 1;
}
var f1 = new F();
f1.__proto === F.prototype; // true

ES5里的arguments, caller和callee

注意: callee和caller以及arguments已在ES6里废弃

关于arguments.callee指向当前函数 [function].caller指向调用该函数的外层函数

1
2
3
function A() { console.log(this.constructor.caller); }
function B() { new A(); // 相当于把arguments.callee传入A }
B(); // 输出的是function B() {...},因为this.constructor指向的是构造函数

在ES6中虽然废弃了arguments,可以使用解构来接收参数:

1
2
3
4
5
6
7
8
9
10
11
12
// 求和
let add = (x, y) => x + y;
let sum = (...args) => {
if (args.length < 2) {
return args[0] || 0;
}
return [].reduce.call(args, add);
};
// test cases
sum(); // 0
sum(2); // 2
sum(1, 2, 4); // 7

Yoda尤达表达式

其实就是我们常见的那种条件判断:

1
2
3
4
5
6
7
String str = null;
if (str.equals("foobar")) {}
// this will cause a NullPointerException in Java
// Using Yoda conditions
if ("foobar".equals(str)) {}
// this is safe, as expected

Yoda表达式的优点:

  1. 可以避免一些问题.比如if (num = 42)无意识会将42赋值给num,改变了num的值,而采用Yoda表达式if (42 = num)则编译错误.
  2. 解决了上例中不安全的空指针隐患.

当然Yoda条件也有一些缺点:

  1. 可读性差.(批评者认为弊大于利!)

而在另外一些语言里比如Python,Swif禁止条件判断力包含赋值语句,通过赋值语句不返回值来避免这个问题.一般认为Yoda表示法是为了变通一些语言的设计,而这些设计可能会带来一些小问题,比如用=表示赋值,
==表示比较,开发者容易将比较写成=,由此容易引发一些问题。所以网路上一般不推荐使用Yoda表示法.综上在js里,Yoda表示法还是有用的!值得注意的是,一个良好的编程习惯应当尽量不要在条件判断里写赋值语句!
详细请参考以下链接:

  1. 维基百科Yoda Conditions
  2. 王垠 Yoda表示法错在哪里

对象深拷贝

常规的做法就是递归遍历每个属性,然后对每个属性进行拷贝。以前我以为只要Array、Function、Object、基本值类型这几类拷贝就行了,然而还是too young too naive!
包括jquery和lodash(undersocre)在内的一些库对于深拷贝的实现都有很多行代码。如jquery的有六十来行(当然jQuery也没有处理好循环引用的问题),而lodash有上百行,可见水不浅!
当然如果对象里只包含Number, String, Boolean, Array, null等扁平对象,即那些能够被 json 直接表示的数据结构,则可以使用JSON.parse(JSON.stringify(obj))来进行深拷贝
参考链接:深入剖析 JavaScript的深拷贝

其它

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 取整
parseInt(a, 10);
Math.floor(a);
a >> 0;
~~a;
a|0
// undefined,数组或对象访问都比较慢,慎用!
undefined === void 0 === 0[0] === ({}).a;
// Infinity
Infinity === 1/0;
// 布尔值转化
true === !0 === !!('1' == 1)
// 时间戳
+new Date()

/ 后面加上的 /
其他相关参考文章
浏览器控件重绘问题概述浏览器重排、重绘、渲染机制
要点:因为浏览器优化,会将某些操作合并到一起,如果属于不同操作都可能触发浏览器重回,所以尽量把同类型操作写一起!