概述

放假读完了《你不知道的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 <---- 应用了软绑定

《你不知道的javascript》读书笔记1的更多相关文章

  1. 《Linux/Unix系统编程手册》读书笔记 目录

    <Linux/Unix系统编程手册>读书笔记1  (创建于4月3日,最后更新4月7日) <Linux/Unix系统编程手册>读书笔记2  (创建于4月9日,最后更新4月10日) ...

  2. 《Linux/Unix系统编程手册》读书笔记9(文件属性)

    <Linux/Unix系统编程手册>读书笔记 目录 在Linux里,万物皆文件.所以文件系统在Linux系统占有重要的地位.本文主要介绍的是文件的属性,只是稍微提及一下文件系统,日后如果有 ...

  3. 《Linux/Unix系统编程手册》读书笔记8 (文件I/O缓冲)

    <Linux/Unix系统编程手册>读书笔记 目录 第13章 这章主要将了关于文件I/O的缓冲. 系统I/O调用(即内核)和C语言标准库I/O函数(即stdio函数)在对磁盘进行操作的时候 ...

  4. 《Linux/Unix系统编程手册》读书笔记7 (/proc文件的简介和运用)

    <Linux/Unix系统编程手册>读书笔记 目录 第11章 这章主要讲了关于Linux和UNIX的系统资源的限制. 关于限制都存在一个最小值,这些最小值为<limits.h> ...

  5. 《Linux/Unix系统编程手册》读书笔记6

    <Linux/Unix系统编程手册>读书笔记 目录 第9章 这章主要讲了一堆关于进程的ID.实际用户(组)ID.有效用户(组)ID.保存设置用户(组)ID.文件系统用户(组)ID.和辅助组 ...

  6. 《Linux/Unix系统编程手册》读书笔记5

    <Linux/Unix系统编程手册>读书笔记 目录 第8章 本章讲了用户和组,还有记录用户的密码文件/etc/passwd,shadow密码文件/etc/shadow还有组文件/etc/g ...

  7. 《Linux/Unix系统编程手册》读书笔记4

    <Linux/Unix系统编程手册>读书笔记 目录 第7章: 内存分配 通过增加堆的大小分配内存,通过提升program break位置的高度来分配内存. 基本学过C语言的都用过mallo ...

  8. 《Linux/Unix系统编程手册》读书笔记3

    <Linux/Unix系统编程手册>读书笔记 目录 第6章 这章讲进程.虚拟内存和环境变量等. 进程是一个可执行程序的实例.一个程序可以创建很多进程. 进程是由内核定义的抽象实体,内核为此 ...

  9. 《Linux/Unix系统编程手册》读书笔记1

    <Linux/Unix系统编程手册>读书笔记 目录 最近这一个月在看<Linux/Unix系统编程手册>,在学习关于Linux的系统编程.之前学习Linux的时候就打算写关于L ...

  10. 《Linux/Unix系统编程手册》读书笔记2

    <Linux/Unix系统编程手册>读书笔记 目录 第5章: 主要介绍了文件I/O更深入的一些内容. 原子操作,将一个系统调用所要完成的所有动作作为一个不可中断的操作,一次性执行:这样可以 ...

随机推荐

  1. git clone Failed to connect to 127.0.0.1 port 43213: Connection refused

    不知道为什么使用git clone 的时候报了上面的错误,后面发现是 127.0.0.1 port 43213的端口被代理占用了,可以这样查看: $ env|grep -i proxy 结果是: NO ...

  2. Jmeter正则表达式提取器二(转载)

    转载自 http://www.cnblogs.com/qmfsun/p/5906462.html JMeter获取正则表达式中的提取的所有关联值的解决方法: 需求如下: { : "error ...

  3. 十七、 Observer 观察者设计模式

    设计: 代码清单: Observer public interface Observer { void update(NumberGenerator generator); } DigitObserv ...

  4. NodeJs命令

    cd命令,就是change directory的缩写,表示更改当前目录 cls命令,清屏.清屏幕命令(CLS,CLear Screen) tab键,自动补全. 上键,提示最近的命令   在cmd窗口 ...

  5. HTML中的Meta标签详解

    emta标签的组成:meta标签分两大部分:HTTP-EQUIV和NAME变量. HTTP-EQUIV:HTTP-EQUIV类似于HTTP的头部协议,它回应给浏览器一些有用的信息,以帮助正确和精确地显 ...

  6. 转)Ubuntu16.04下安装DDD(Data Display Debugger)

    以下转自:http://www.linuxdiyf.com/linux/26393.html   前两天在Linux论坛偶然间看到了DDD这个软件,根据介绍是一个gdb界面化的调试软件,这正是我找了好 ...

  7. 2017-2018-2 20165315 实验四《Android程序设计》实验报告

    2017-2018-2 20165315 实验四<Android程序设计>实验报告 第24章:初识Android Android Studio项目的目录树 1 build:该目录包含了自动 ...

  8. Linux下Mysql安装(tar安装)

    1.为数据库创建软件目录以及数据存放目录 #mysql软件目录 mkdir /software/ #mysql数据文件目录 mkdir /data/mysql 2.上传mysql-XXXXXX.tar ...

  9. Linux知识扩展二:lsof命令

    转:https://www.cnblogs.com/the-study-of-linux/p/5501593.html 1. lsof :list open file 显示linux下打开的文件信息. ...

  10. tiny4412 --uboot移植(1)

    开发环境:win10 64位 + VMware12 + Ubuntu14.04 32位 工具链:linaro提供的gcc-linaro-6.1.1-2016.08-x86_64_arm-linux-g ...