• 《你不知道的javascript》读书笔记1


    概述

    放假读完了《你不知道的javascript》上篇,学到了很多东西,记录下来,供以后开发时参考,相信对其他人也有用。

    js的工作原理

    • 引擎:从头到尾负责整个js的编译和运行。(很大一部分是查找操作,因此比如二分查找等查找方法才这么重要。)
    • 编译器:负责语法分析和代码生成。
    • 作用域:收集所有声明的变量,并且确认当前代码对这些变量的访问权限。

    LHS查询和RHS查询:

    • LHS查询:当变量出现在赋值操作左边时,会发生LHS查询,如果LHS查询不到,那么会新建一个变量。严格模式下,如果这个变量是全局变量,就会报ReferenceError。
    • RHS查询:当变量出现在赋值操作右边时,会发生RHS查询,如果RHS查询不到,那么会报ReferenceError错误。

    TypeError和Undefined:

    • TypeError:当RHS查询成功,但是对变量进行不合理的操作时,就会报TypeError错误,意思是作用域判别成功了,但是操作不合法。
    • Undefined:当RHS查询成功,但是变量是在LHS查询中自动新建的,并没有被赋值,就会报Undefined错误,意思是没有初始化。
    //下面这段代码使用了3处LHS查询和4处RHS查询
    function foo(a) {
        var b = a;
        return a + b;
    }
    var c = foo( 2 );
    

    欺骗词法

    js中使用的作用域是词法作用域,意思是变量和块的作用域是由你把它们写在代码里的位置决定的。还有一种是动态作用域,意思是作用域是程序运行的时候动态决定的,比如Bash脚本,Perl等。下面的代码在词法作用域中会输出2,在动态作用域中会输出3。

    function foo() {
        console.log( a );
    }
    function bar() {
        var a = 3;
        foo();
    }
    var a = 2;
    bar();
    

    有2种方法欺骗词法作用域,一个是eval,另一个是with,这也是它们被设计出来的目的。需要注意的是,欺骗词法作用域会导致性能下降,因为当编译器遇到它们的时候,会放弃提前设定好他们的作用域,而是需要在运行的时候由引擎来动态推测它们的作用域。

    eval()接受一个字符串,这个字符串是一段代码,执行的时候,这段代码中的变量定义会修改当前eval()函数所在的作用域。在严格模式下,eval()函数有自己的作用域,里面的代码不能修改eval()函数所在的作用域。

    //修改foo函数中的作用域,使b=3
    function foo(str, a) {
        eval( str ); // 欺骗!
        console.log( a, b );
    }
    var b = 2;
    foo( "var b = 3;", 1 ); // 1, 3
    
    //严格模式下,eval()函数有自己的作用域
    function foo(str) {
        "use strict";
        eval( str );
        console.log( a ); // ReferenceError: a is not defined
    }
    foo( "var a = 2" );
    

    with可以把一个对象处理为单独的完全隔离的作用域,它的本意是被当做重复引用同一个对象中的多个属性的快捷方式,但是由于LHS查询,如果对象中没有这个属性的时候,会在全局中创建一个这个属性。在严格模式下,with被完全禁止使用。

    function foo(obj) {
        with (obj) {
            a = 2;
        }
    }
    var o1 = {
        a: 3
    };
    var o2 = {
        b: 3
    };
    foo( o1 );
    console.log( o1.a ); // 2
    foo( o2 );
    console.log( o2.a ); // undefined
    console.log( a ); // 2——不好,a 被泄漏到全局作用域上了!
    

    匿名函数表达式

    匿名函数表达式是一个没有名称标识符的函数表达式,比如下面的:

    setTimeout( function() {
        console.log("I waited 1 second!");
    }, 1000 );
    

    匿名函数表达式有很多缺点:

    1. 匿名表达式不会在栈追踪中显示出有意义的函数名,使得调试很困难。
    2. 由于没有函数名,所以当想要引用自身的时候只能用arguments.callee,而这又会倒置很多问题。
    3. 匿名函数影响了可读性。一个描述性的名称,可以让代码梗易读。

    所以最好始终给函数表达式命名。上面的代码可以改成如下所示:

    setTimeout( function timeoutHandler() { // <-- 快看,我有名字了!
        console.log( "I waited 1 second!" );
    }, 1000 );
    

    IIFE

    之前我在博文中说明过IIFE,所以这里只补充一个IIFE的其它用途,就是传入一些特殊的值。

    //传入undefined
    undefined = true; // 给其他代码挖了一个大坑!绝对不要这样做!
    (function IIFE( undefined ) {
        var a;
        if (a === undefined) {
            console.log( "Undefined is safe here!" );
        }
    })();
    
    //传入this
    (function IIFE( this ) {
        console.log( this.a );
    }
    })(this);
    

    显式的块作用域

    有时候,可以把一段代码显式地用块包起来,这样写能够更易读,也更容易释放内存。如下所示:

    function process(data) {
        // 在这里做点有趣的事情
    }
    // 在这个块中定义的内容可以销毁了!
    {
        let someReallyBigData = { .. };
        process( someReallyBigData );
    }
    var btn = document.getElementById( "my_button" );
    btn.addEventListener( "click", function click(evt){
        console.log("button clicked");
    }, /*capturingPhase=*/false );
    

    代码缺陷

    for (var i=1; i<=5; i++) {
        setTimeout( function timer() {
            console.log( i );
        }, i*1000 );
    }
    

    上面的例子是一个很常见的前端面试题。我们来深入研究一下。

    首先是写出这段代码我们期望什么?我们期望每一个循环中都有一个当前i的副本被绑定到setTimeout函数里面,所以当setTimeout函数执行的时候,会输出不同的i值。

    但是事实并不是这样的,一个原因是setTimeout函数是异步的,另一个原因是所有的setTimeout函数所在的作用域都是全局作用域,这个全局作用域中只有一个i(只有函数作用域的代码缺陷)。

    所以解决方法是给每一个setTimeout函数创建一个独自的作用域,可以用闭包创建函数作用域,也可以用let创建块作用域。

    现代的模块机制

    现代的模块机制有AMD模块机制和CMD模块机制。前者是在模块执行之前加载依赖模块,后者是在模块执行的时候动态加载依赖模块。

    下面是AMD模块机制的模块加载器。需要注意的是deps[i] = modules[deps[i]];作用是加载依赖模块,modules[name] = impl.apply( impl, deps );作用是加载模块impl。

    //通用的模块加载器
    var MyModules = (function Manager() {
        var modules = {};
        function define(name, deps, impl) {
            for (var i=0; i<deps.length; i++) {
                deps[i] = modules[deps[i]];
            }
            modules[name] = impl.apply( impl, deps );
        }
        function get(name) {
            return modules[name];
        }
        return {
            define: define,
            get: get
        };
    })();
    

    为什么modules[name] = impl.apply( impl, deps );不写成modules[name] = impl( deps );?为什么要给自己传一个自己的this指针进去?原因是如果不传进去的话,impl( deps )中的this会指向全局作用域!

    未来的模块机制

    最新的es6的模块机制是这样的:

    bar.js
    function hello(who) {
        return "Let me introduce: " + who;
    }
    export hello;
    foo.js
    // 从 "bar" 模块导入 hello()
    import hello from "bar";
    var hungry = "hippo";
    function awesome() {
        console.log(
            hello( hungry ).toUpperCase()
        );
    }
    export awesome;
    

    闭包

    看完本书之后感觉自己对闭包的理解还是不够深刻。闭包真正的理解是:当函数在当前作用域之外执行的时候,它仍然能够访问自己原本所在的作用域,这个时候就出现了闭包。

    在哪些地方用到了闭包?闭包在不污染全局变量,定义模块和立即执行函数方面有很多运用,特别要注意的是,所有异步操作中的回调函数都使用了闭包。比如定时器,事件监听器,Ajax请求,跨窗口通信,WebWorkers等。因为在异步编程中,回调函数一般是在代码执行完毕之后再执行的,这个时候怎么记住回调函数里面的各种参数(即回调函数的作用域)?当然是用闭包啦。

    另一点需要注意的是,回调函数会丢失this。比如下面的代码:

    function foo() {
        console.log( this.a );
    }
    var obj = {
        a: 2,
        foo: foo
    };
    var a = "oops, global"; // a 是全局对象的属性
    setTimeout( obj.foo, 100 ); // "oops, global"
    

    虽然foo函数执行的时候,前面有一个obj,但是foo里面的this指向的却是全局对象,原因是setTimeout()函数的伪代码其实是如下所示的,它执行了这个操作fn=obj.foo;fn()。所以实际调用的是fn()函数。(同时也可以很明显的看出,foo函数并没有在它定义的那个作用域执行,而是跑到了setTimeout的作用域,所以出现了闭包。)

    function setTimeout(fn,delay) {
    // 等待 delay 毫秒
    fn(); // <-- 调用位置!
    }
    

    this

    this设计的初衷是提供一种更优雅的方式来隐式“传递”一个对象引用,因此可以将API设计的更加简洁并且易于复用。

    判断this的指向:

    1. 函数是否在 new 中调用( new 绑定)?如果是的话 this 绑定的是新创建的对象。var bar = new foo()
    2. 函数是否通过 call 、 apply (显式绑定)或者硬绑定调用?如果是的话, this 绑定的是
      指定的对象。var bar = foo.call(obj2)
    3. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话, this 绑定的是那个上
      下文对象。var bar = obj1.foo()
    4. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined ,否则绑定到
      全局对象。var bar = foo()

    值得说明的是,箭头函数并没有使用上面的规则,而是根据外层的作用域来决定this。所以箭头函数常用于回调函数中(因为回调函数丢失了this,会造成很多错误)。

    另外Function.prototype.bind()函数使用了上面的规则,只不过强制把this绑定到定义的作用域上面。它与箭头函数有着本质的不同。

    使用apply展开数组

    下面的例子是使用apply把数组展开为参数。es6中可以用...展开数组。

    function foo(a,b) {
        console.log( "a:" + a + ", b:" + b );
    }
    // 把数组“展开”成参数
    foo.apply( null, [2, 3] ); // a:2, b:3
    // 使用 bind(..) 进行柯里化
    var bar = foo.bind( null, 2 );
    bar( 3 ); // a:2, b:3
    

    更安全的this

    一个非常安全的做法是把this绑定到一个不会对程序造成任何影响的空对象上面,而Object.create(null)和{}很像,但是并不会创建Object.
    prototype这个委托,所以它比{}“更空”,所以一般把this绑定到Object.create(null)对象上面。不过es6规定的严格模式对这种情况有缓解。

    function foo(a,b) {
        console.log( "a:" + a + ", b:" + b );
    }
    // 我们的 DMZ 空对象
    var ø = Object.create( null );
    // 把数组展开成参数
    foo.apply( ø, [2, 3] ); // a:2, b:3
    // 使用 bind(..) 进行柯里化
    var bar = foo.bind( ø, 2 );
    bar( 3 ); // a:2, b:3
    

    软绑定

    这里介绍一种软绑定,只把代码放在下面,代码我还没有看懂。。。。

    //软绑定函数softBind
    if (!Function.prototype.softBind) {
        Function.prototype.softBind = function(obj) {
            var fn = this;
            // 捕获所有 curried 参数
            var curried = [].slice.call( arguments, 1 );
            var bound = function() {
                return fn.apply(
                    (!this || this === (window || global)) ?
                    obj : this
                    curried.concat.apply( curried, arguments )
                );
            };
        bound.prototype = Object.create( fn.prototype );
        return bound;
        };
    }
    
    //软绑定例子
    function foo() {
        console.log("name: " + this.name);
    }
    var obj = { name: "obj" },
    obj2 = { name: "obj2" },
    obj3 = { name: "obj3" };
    var fooOBJ = foo.softBind( obj );
    fooOBJ(); // name: obj
    obj2.foo = foo.softBind(obj);
    obj2.foo(); // name: obj2 <---- 看!!!
    fooOBJ.call( obj3 ); // name: obj3 <---- 看!
    setTimeout( obj2.foo, 10 );
    // name: obj <---- 应用了软绑定
    
  • 相关阅读:
    如何使用angularjs实现文本框设置值
    如何使用angularjs实现文本框获取焦点
    electron的安装
    linux中升级jdk的方法
    linux中添加开机自启服务的方法
    liunx系统安装tomcat的方法
    liunx系统安装jdk的方法
    常用linux命令
    ResourceBundle的使用
    查看Linux系统版本的命令
  • 原文地址:https://www.cnblogs.com/yangzhou33/p/8729349.html
Copyright © 2020-2023  润新知