概述

放假读完了《你不知道的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. XGBoost参数调优小结

    https://mp.weixin.qq.com/s?__biz=MzU0MDQ1NjAzNg==&mid=2247485630&idx=1&sn=9edf2bfd771cf4 ...

  2. canvas刮刮乐游戏等

    裁剪 ctx.clip():当前路径外的区域不再绘制 <canvas id="cans" width=500 height=500></canvas> &l ...

  3. R语言-增加图例

    legend()函数 > plot(rain$Tokyo,type="l",col="red", + ylim=c(0,300), + main=&quo ...

  4. 打开控制台F12弹出弹窗

    window.onload=function(){                 document.onkeydown=function(){                     var e=w ...

  5. oracle 连接字符串的问题

    未指定的错误,发生了一个 Oracle 错误,但无法从 Oracle 中检索错误信息.数据类型不被支持. 原因是你用的ADO   for   ORACLE的驱动是微软的Microsoft OLE DB ...

  6. Tomcat9 在Windows中配置允许远程访问

    环境:Windows  Server 2019 Data Center+Tomcat 9 Tomcat在Windows中安装好了之后,默认只能从本机以http://localhost:8080的方式访 ...

  7. object references an unsaved transient instance save the transient instance before flushing

    object references an unsaved transient instance save the transient instance before flushing 对象引用未保存的 ...

  8. 创建第一次C语言程序

    在这里我以VS2015为例,做演示.为什么要去演示怎样创建项目尼,因为我写第一个程序时,不知道该怎样用VS创建我的第一个应用程序. 第一步:打开VS环境如下 第二步:在开始出点击“新建项目”或在右上角 ...

  9. 247. Strobogrammatic Number II输出所有对称数字

    [抄题]: A strobogrammatic number is a number that looks the same when rotated 180 degrees (looked at u ...

  10. 彻底关闭win10后台同步数据(转自技术社区)

    设置隐私里面关闭所有同步数据选项 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\将下面子项属性修改 OneSyncSvc的start属相修改 ...