JavaScript原理(一)

一、函数解析

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. 比较ilength的值。
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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值