一、函数解析
JavaScript解析是一段一段,并非一行一行解析。同一段中function语句和函数直接量定义的函数总会被优先编译执行(该执行不是调用函数),之后才会执行其他函数。new Function()在运行时动态地被执行(导致作用域也不同于前者)。
前两者基本相同,因为被优先编译处理,new耗时非常高,每次循环都动态编译
// 三种函数创建的速度测试
var zz = new Date();
var st = zz.getTime();
for (var i = 0; i < 10000; i ++) {
var foo = function () {;}
function foo() {;}
// var kk = new Function()
}
var cc = new Date();
var str = cc.getTime();
console.log(str - st);
- new Function() JavaScript总是把它做为顶级作用域进行编译。
- 函数直接量,和new的形式,节省资源,避免了function语句形式占内存的弊端。
var n = 3;
function cc() {
var n = 5;
var m = new Function('return n');
return m;
}
console.log(cc()()) // 3 而不是 5
二、动态调用函数
call 、apply会改变this指针。 可以把一个函数转化为方法传递给对象。但是这种是临时的,方法执行后自动销毁,对象并没有该方法。
var obj = {
aa: 3
}
function d () {
console.log(this.aa)
}
// d函数临时做为对象obj的方法,this被指向obj。obj的aa是3
d.call(obj) // 3
d.apply(obj) // 3
返回数组最大值:
// 下面都能实现,其实是动态调用了Math.max()的方法。
var arr = [4, 6, 9,33]
console.log(Math.max.apply(undefined, arr))
console.log(Math.max.apply(null, arr))
console.log(Math.max.apply(Object, arr))
三、函数引用,调用
遵照1、2两条,能正确识别函数是怎么调用的,则this的指向也不是问题。
1.函数调用(this指向window) add(2, 3)
2.方法调用(做为对象方法调用,this指向对象本身) obj.add()
3.构造器调用。new Foo()
// 1.创建一个空对象。
var obj ={};
// 2.设置这个对象的原型,就是指定__proto__的指向
obj.__proto__ = Foo.prototype;
// 3.将构造函数的作用域赋给新对象(因此this就指向了新对象)
Foo.call(obj);
4.执行构造函数中的代码(给这个新对象添加方法和属性)
5.返回这个对象(this)
return obj;
4.动态调用,call、apply,会改变this指针。
四、JavaScript预编译过程
解释型语言:代码在执行时才被解释器逐行动态编译和执行,而不是在执行前就完成编译。
编译型语言:先编译后执行。
JS解析过程分两步,编译和执行。
编译:JS的预编译(预处理),把JS脚本转化为字节码。
执行:JS借助执行环节,将字节码转换为机械码,并按顺序执行。
五、非惰性求值和惰性求值
非惰性
- 非惰性:不管表达式是否被应用,只要在执行代码中都会被计算。
- 函数作为运算符号参与运算时,具有非惰性求值特性。
var a = 3;
function f (arg) {
return arg;
}
console.log(f(a,a = a * a)); // 3
console.log(f(a)); // 9
惰性
- 对函数或请求的处理延迟到真正需要结果时在进行处理。它的目的是要最小化计算机要做的工作。
// 每次调用f都会重新求值
var t;
function f() {
t = t ? t : new Date();
return t;
}
f()
改进:
var f = function () {
console.log(33) // 只执行一次
var t = new Date() // 只执行一次
f = function () {
return t;
}
return f();
}
console.log(f())
console.log(f())
console.log(f())
console.log(f())
console.log(f())
六、循环性能
1、每次迭代做什么。
2、迭代次数。
var arr = [];
for (var i = 0; i < arr.length; i ++) {
}
var j = 0;
while (j < arr.length) {
}
var k = 0;
do {
} while (k < arr.length);
步驟:
1. 每次读取length
2. 比较i 与 length的值。
3. 比较操作 i < arr.length == true;是否成立
4. 一次 ++ 操作
5. 循环内的语句。
优化上述:
1. length是固定的,可以用变量缓存var length = arr.length
,这样length只需读取一次。
2. 倒叙循环方式,到循环的方式,将上面的2、3合成了一步(I == true,非零数字转化为true)。
for (var i = arr.length; i --) {
}
var j = arr.length;
while(j --) {
}
var k = arr.length - 1;
do {
} while (k --);
3、 for 循环中避免声明变量。
for (var i = arr.length; i --) {
var arr = []
}
注意:基于函数的迭代,性能相对较差。forEach()的函数。
七、条件性能
1.条件较少时,使用if,较多时用switch。大多数情况switch性能优于if。只有当条件多时更加明显。
2.优化if的逻辑。条件的写法遵从最大概率到最小概率的写法。
if (num < 5) {
} else if ( num >= 5 && num < 10) {
} else {
}
如果你的条件出现在小于5的概率最大,那就将它放到最前面。这样就可能少去了二次或三次判断。
3.多条件成立下,才执行语句。
if (a) {
if(b) {
}
}
// 不要多层嵌套。下面这个更合理。
if (a && b) {
}
// 同理,如果a b条件都不成立,也不要写多次嵌套判断。
if (!(a && b)) {
}
4.部分情况,可以采用查表法,代替if
if (1) {
console.log('元/每吨')
}
if (2) {
console.log('元/方')
}
// 这是用了对象, 当然也可以用数组。远远优于条件语句,并且便于理解。
function foo(arg) {
var obj = {
'1': '元/每吨',
'2': '元/方'
}
return obj[arg]
}
// 数组相对于对象,稍有局限,就是arg是数值。
function checkList(arg) {
var arr = ['元/每吨', '元/方']
return arr[arg]
}
隐藏问题:
// 该语句正常输出3,但是本意是想判断 a == 1 是否成立,然而下面情况正常运行。没有报错,条件却不是因为 a == 1 而成立的,这种问题就非常难以查出来。
if (a = 1) {
console.log(3)
}
// 所以,可以采用变量在又,常量在左。如果下面少写了等号,将会报错。
if (1 == a) {
console.log(3)
}
5.for in的性能相对较差。 for in每次迭代要搜索实例或原型属性。因此付出更多开销。除非要对数目不祥的属性进行操作。否则尽量便面使用for in
八、递归。
优点:
1.一个简单的阶乘递归,递归的速度非常快。
2.在进行复杂的算法时,递归也相对方便。
3.用递归实现的算法,都可以用迭代(迭代会有一定的性能损耗)。
缺点:
1.错误或缺少终止条件会导致浏览器假死。
2.受到调用栈的大小影响。如果超出提示错误。Uncaught RangeError: Maximum call stack size exceeded
function aa (n) {
if (n === 0) {
return 1
} else {
return n * aa(n-1)
}
}
// try catch 捕获错误
try {
aa(11111111111111110)
} catch (ex) {
console.log('dfsdfsdf')
}
采用制表优化递归。
在函数内部建立一个缓存对象,预制两个简单阶乘。(递归的流程可以打断点自己研究)
var i = 0;
function aaa (n) {
console.log(i ++)
if (!aaa.cache) {
aaa.cache = {
'0': 1,
'1': 1
}
}
if (!aaa.cache.hasOwnProperty(n)) {
aaa.cache[n] = n * aaa(n-1);
}
return aaa.cache[n]
}
// 因为在计算6的阶乘时,已经计算了5和4的。所以缓存中已存在,所以上述函数只执行了8次(变量i)。
var dd = aaa(6)
var ee = aaa(5)
var ff = aaa(4)
// console.log(dd, ee, ff)
九、字符
1、replace的第二个参数推荐采用function。
var str = 'JavaScript'
str.replace(/(Sc)(ri)(pt)/g, function (v1, v2, v3, v4) {
// argments[0] : 匹配的内容
// argments[1] : 第一个自表达式匹配的内容
// argments[2] : 第二个自表达式匹配的内容
// argments[3] : 第三个自表达式匹配的内容
console.log(v1, v2, v3, v4);
})
2、正则机制。
1.编译:创建一个正则,首先要编译,所以多次调用同一个正则时,将其存储变量,可以减少不必要的编译操作。
2.设置起始位置:正则使用时,要先确定目标字符串开始搜索位置(字符串开始位置或正则的lastIndex指定的位置。)
3.配置正则字符:设置好起始位置后,正则将会一个一个扫描目标文本和正则表达式模版,当一个失败时,正则试图回溯到扫描之前的位置,然后进入其他的可能路径。
4.成功或失败:若匹配到相同的,则匹配成功。若失败则回溯到第二步,从下一个字符重新尝试。
3、在同一个正则表达式内,可以用\后加一位或多位数字实现。\1 是第一个带括号的子表达式
var str = 'liu "yong!" yong! shun ';
var str2 = 'liu "yong!" shun ';
var q = /"([a-z]+\!)" \1/g;
console.log(q.test(str));
console.log(q.test(str2));
十、数组下标
数组下标不一定是正整数,也可以是如下的特殊字符,虽然不合语法但是可以用正常使用。其实对象的访问除了打点,还有一种访问方式,就是中括号的方式,而中括号的方式访问是可以使用变量的(非常有用)。
这种存储方式是哈希表,哈希表的访问(查表)要优于遍历数组。
var arr = [];
arr[false] = 3;
arr[-1] = 2;
console.log(arr) // [false: 3, -1: 2]
console.log(arr[false]) // 3
console.log(arr[-1]) // 2
console.log(arr.length) // 0