读《你不知道的javascript》(上)部分东西记录

你不知道的javascript(上) 一、作用域与闭包 1.编译原理 一般编译分为三个步骤:
a.分词、词法分析(Tokenizing/Lexing)

这个过程会将整个代码(字符组成的字符串)分解成有意义的代码(对编程语言来说),这些代码块被称为词法单元(token)。

例如:代码块 var a = 2; 这段程序通常会被分解成下面这些词法单元:var、a、=、2、; 。空格是否会被当做词法单元,取决于空格在这门语言中是否具有意义。
b.解析/语法分析(Parsing)
这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的数。这个数被称为"抽象语法树"(Abstract Syntax Tree,AST)

例如:var a = 2; 的抽象语法树中,可能会生成如下图所示的AST:
读《你不知道的javascript》(上)部分东西记录
文章图片

c.代码生成
这个过程会将AST转换成为可执行代码,这个过程与语言、目标平台等息息相关。

抛开具体细节,就是通过某种方法,将var a = 2; 的AST转换为一组机器指令,用来创建一个叫做a的变量(包括内存分配),然后将2存储其中。
2.理解作用域 解释代码需要理解的三个演员:
引擎:重头到尾负责整个JavaScript程序的编译以及执行过程。
编译器:负责语法分析及代码生成。
作用域:负责收集并维护所有声明的标识符(变量)组成的一系列查询(作用域链的生成)。
3.词法作用域 词法作用域意味着作用域是由书写代码时变量的声明位置所决定的。因此当词法分析器处理代码时会保持作用域不变(大部分情况下是这样的)。特殊情况下,修改或影响词法作用域会导性能下降。
a.查找 在多层的嵌套作用域中可以定义同名的标识符,这就叫做"屏蔽效应",该效果的达成依赖于作用域链
b.欺骗词法 使用eval(..)函数,用来动态执创建的代码,当改代码中出现变量声明时,就会改变原有的词法作用域,类似的还有setTimout(..)第一个参数(写字符串时),newFunction的第二个参数(写字符串时),它们所带来的好处无法抵消性能上的损失。
例如:
var a = 10; function fn(str){ eval(str)// 修改了原本的词法作用域 console.log(a); // 20 } fn('var a = 20; ')

使用with关键字,创建新的词法作用域
function fn(obj){ with(obj){ a = 99// 当 obj是b时,a被添加到了全局对象中 } } var o1 = {a:1} var o2 = {b:2} fn(o1); fn(o2); console.log(o1.a); console.log(o2.a); console.log(window.a)

c.性能 JavaScript引擎会在编译阶阶段进行数项的性能优化,其中就有一些优化依赖于能够根据代码的词法进行静态分析,并预先确定所有变量和函数的位置,才能在执行过程中快速的找到标识符。所以,当引擎在代码中发现了eval(..)或with,它就只能简单的假设预先对标识符位置的判断都是无效的,所有的优化都可能是无意义的,因此最简单的做法就是完全不做任何优化。建议不要使用它们。
4.函数作用域与块级作用域 a.隐藏内部实现 遵循最小特权原则,最小限度的暴露必要内容。将部分代码封装在函数中,形成一个函数作用域。
b.立即执行函数表达式 立即执行函数表达式(Immediately Invoked Function Expression,IIFE)
使用:
var a = 10; (function IIFE(global){ var a = 20; console.log(a); // 20 console.log(global.a); // 10 })(window)

c.块级作用域 let、const所声明的变量会隐式的绑定在一个已经存在了的作用域上,通常是{..}内部,块级作用域,也有利于对不再使用的变量回收。
使用:
{ let a = 10; var b = 1; const c = 99; } // 但运行到这里是,a与c都会被gc回收console.log(b); console.log(a); // 会报错 console.log(c);

5.声明提升 例如:var a = 2; 会被当作var a和a = 2单个单独的声明,第一个是编译阶段的任务,第二个是执行阶段的任务,通过预编译四部曲,而可以达到符合js中变量提升的效果:
1.创建GO/AO对象 2.找形参和变量声明,将变量和形参名作为AO属性名,值为undefined 3.将实参值和形参统一4.在函数体里面找函数声明,值赋予函数体

6.作用域闭包 a.理解闭包
闭包定义:当函数可以记住并访问所在它的词法作用域时,就产生了闭包,即使函数是在当前此法作用域之外执行。

【读《你不知道的javascript》(上)部分东西记录】闭包展示:
function foo() { var a = 2; function bar() { console.log(a); } return bar; } var baz = foo(); baz(); // 2这就是闭包的效果

b.闭包与循环 问题代码:
for (var i = 0; i < 6; i++) { setTimeout(function timer() { console.log(i); }, i * 1000); }

这段代码,期望是生成数字1~5,每秒一个,但结果却是5个6,这个6的来源是,当跳出循环后,i=6,因为这里的定时器回调函数共用了同一个词法作用域的变量i,而且setTimeout又是宏任务,是在循环完成之后执行的,所有就打印了5个6;
知道了缺陷的原因,这里利用IIFE解决该缺陷,使用IIFE给每一次的循环,都创建一个独立的词法作用域,这样就达到了期望的效果
for (var i = 1; i < 6; i++) { (function IIEF(i) { setTimeout(function timer() { console.log(i); }, i * 1000); })(i) }

也可以使用let声明变量,这样let声明的变量每次也就能依附于一个块级作用域,就是每次循环的{..}上,也能达到期望的效果:
for (let i = 1; i < 6; i++) { setTimeout(function () { console.log(i); }, i * 1000); }

for循环头部的let声明还会有一个特殊的行为,变量在循环过程中,每次迭代都会声明,随后的每次迭代都会使用上次迭代结束时的值来初始化这个变量。
c.闭包 利用闭包特性,作用一个简易的模块管理器:
var MyModules = (function Menager() { 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 { get, define } })(); MyModules.define('bar',[],function(){ function hello(who){ return 'Let me introduce: ' + who; }return { hello } }); MyModules.define('foo',['bar'],function(bar){//当函数中只有一个元素时, bar == [bar] var hungry = 'hippo'; function awesome(){ console.log(bar.hello(hungry).toUpperCase()); }return { awesome } })var bar = MyModules.get('bar'); console.log(bar.hello('jiang')); var foo = MyModules.get('foo'); foo.awesome();

未来模块机制,ES6的模块机制:
// bar.js function hello(who){ return "Hello " + who; } export default hello; // foo.js import hello from './bar.js'; var hungry = 'hippo'; function awesome(){ console.log( hello(hungry).toUpperCase() ) } export default awesome; // baz.js import foo from './foo.js'; foo();

使用node命令运行baz.js时,需要间package.json中的type配置改为"module",才可以运行。
7.其它 a.动态作用域 JavaScritpt的作用域就是词法作用域,也是静态作用域,是在写代码(分词时/编译阶段)就确定了的,而动态作用域,this就是很好的体现,需要在运行时才能确定
b.块作用域的替代方案 将ES6的代码转成ES6之前环境能运行的新式:
// ES6 { let a = 2; console.log(a); } console.log(a); // ES6之前 try{throw 2}catch(a){ console.log(a); } console.log(a);

这里为什么不使用IIFE来创建作用域呢?首先,try/catch的性能的确很糟糕(已经做了改进),其次IIFE与try/catch并不完全等价,如果将任意一段代码的一部分拿出来用函数包裹,会改变这段代码的含义,其中this、return、break/continue都会发生变化,所以IIFE并不是一个普适的解决方案,他只适合在某些特定情况下进行手动操作。
c.箭头函数中的this绑定 这里演示函数对象记录自身被调用的次数
var obj = { count: 0, cool: function coolFn() { // setTimeout(() => { //this.count++; //console.log(this.count); // }, 1000); setTimeout((function timer() { console.log(this) this.count++; console.log(this.count); }).bind(this), 1000); } } obj.cool();

二、this
1.this this是在运行时绑定的,它的绑定取决于函数的调用的方式,与函数的声明没有关系。
常见的this绑定的四种方式:
默认绑定:
var a =10; function foo(){ console.log(this.a); // this == .window } foo()

隐式绑定:
function foo(){ console.log(this.a); // 这里的this为obj } var obj = { a: 2, foo: foo } obj.foo();

显示绑定
使用call(..)、apply()、bind(..)函数绑定
function foo(){ console.log(this.a); // 这里的this为obj } var obj = { a: 8, } foo.apply(obj);

new绑定:
使用new关键字,对函数进行构造调用,会自动执行一下操作:
1.在函数首航创建一个全新的对象,例如 var this = {}; 2.该队行会被执行[[原型]]连接 3.这个新对象会绑定到函数调用的this 4.如果被new调用的函数没有显示返回其它对象,则会自动返回一个新对象

代码中使用:
function foo(){ this.a = 10; console.log(this.a); // 这里的this为obj } var obj = new foo();

2.被忽略的this 当把null或者undefined作为this的绑定对象传入call、apply、bind时,这些值在调用时会被忽略,实际运行用的是默认绑定规则:
function foo(){ console.log(this.a); } var a = 2; foo.call(null);

使用apply(..)来展开数组,bind(..)可以实现函数的柯里化(预先设置一些参数):
function foo(a,b){// 展开数组 console.log('a:' + a, 'b:' + b); } foo.apply(null,[2,3]); var bar =foo.bind(null,2); //函数柯里化 bar(3);

总是使用null来忽略this绑定可能会产生一些副作用,当函数内确实使用了this时,this将会绑定到window全局对象,就有可能修改全局对象,这种当然不好。创建一个空的非委托对象代替null,将会更加安全:var DMZ = Object.create(null);
如:
var DMZ = Object.create(null); function foo(a,b){// 展开数组 this.a = 10; console.log('a:' + a, 'b:' + b); } foo.apply(DMZ,[2,3]);

3.软绑定 给默认绑定指定一个全局对象和undefined以外的值,不仅可以实现和硬绑定相同的效果,还同时保留了隐式绑定或者显示绑定修改this的能力。
if (!Function.prototype.softBind) { Function.prototype.softBind = function (obj) { var fn = this; var curried = [].splice.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' }; var obj2 = { name: 'obj2' }; obj2.bar = foo.softBind(obj); obj2.bar(); setTimeout(obj2.bar, 1000);

4.箭头函数 箭头函数的this绑定时书写箭头函数的的位置,根据外层(函数或者全局)作用域来决定,任何方式不能再被修改。
看一个示列代码:
function foo(){ return (a)=>{ console.log(this.a); } }var obj1 = { a:2 }var obj2 = { a:3 }var bar = foo.call(obj1); // 这里已经确定了箭头函数的this指向了obj1,不能再被改变了bar.call(obj2); // 2 而不是 3

在es6之前,使用的类似域箭头函数的模式:
function foo() { var self = this; setTimeout(function () { console.log(self.name); }, 1000); } var obj = { name: 'obj', } foo.call(obj);

三、对象
1.数据类型 JS中基本数据类型:string、number、boolean、object、null、undefined,JS内置对象(内置函数,可new):String、Number、Boolean、Object、Function、Array、Date、RegExp、Error
使用:
var strPrimitive = "I am string"; typeof strPrimitive; // "string" strPrimitive instanceof String; // falsevar strObject = new String("I am String"); typeof strObject; // "object" strObject instanceof String; // true

类型转换,当在使用字面量的属性或方法时,会使用对应的内置构造函数将其包装成对象后调用属性或函数:如
console.log('I am string'.length); // ==> new String('I am string').length; console.log(42.324.toFixed(2)); // ==> new Number(42.324).toFixed()

不同的是,null和undefined没有对应的构造形式它们之后文字形式。相反,Date只有构造,没有文字形式。对于Object、Array、Function、RegExp来说,无论是文字形式还是构造函数新式,它们都是对象。Error对象一般都是出错时被自动创建,也可以使用new Error(..)手动创建,使用时用throw关键字抛出:throw new Error(..)
2.属性描述符 属性描述符包括"数据描述符"与"访问描述符"。
属性描述符:
let obj = Object.create(null); Object.defineProperty(obj, 'name', { value: '张三',// 属性值 writable: false,// 属性是否可修改 configurable: false,// 属性是否可删除,以及属性描述符是否可修改 enumerable: true// 属性是否可枚举 });

访问器描述符(两种方式定义):
var obj = { get name() { return _name_; }, set name(value) { _name_ = value; } } obj.name = 'jiang'; Object.defineProperty(obj,'age',{ get : function(){ return _age_; }, set : function(value){ _age_ = value; } }) obj.age = 18; console.log(obj);

3.遍历 这里介绍一下数组的for..of遍历,for..of循环首先会向被访问对象请求一个迭代器对象@@iterator,然后通过迭代器对象的next()方法来遍历所有返回值:如下
var myArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; var it = myArray[Symbol.iterator](); while (true) { var result = it.next(); if (result.done) break; console.log(result.value); }

但是普通的对象却没有内置的@@iterator,当想要做这样循环的操作,也可以给想遍历的对象定义一个@@iterator对象,如下:
var myObject = { name: "John", age: 34, isMarried: false }; Object.defineProperty(myObject, Symbol.iterator, { enumerable: false, writable: false, configurable: true, value: function () { var o = this; var idx = 0; var ks = Object.keys(o); return { next() { return { value: o[ks[idx++]], done: idx > ks.length } } } } })for (let key of myObject) { console.log(key); }

利用for..of循环每次调用迭代器对象的next()方法时,内部指针都会向前移动并返回对象属性列表的下一个值,利用这个特性制作一个永远不会结束的迭代器(比如返回随机数、递增值、唯一标识符等等):
var randoms = { [Symbol.iterator]:function(){ return { next(){ return { value:Math.random(), done:false } } } } }var random_pool = []; for(let n of randoms){ random_pool.push((n*100).toFixed(2)); if(random_pool.length>=100){ break; } } console.log(random_pool);

四、原型
1.屏蔽与属性设置 JS中,当出现myObject.foo = "bar"赋值操作时会出现的三种情况:
1.如果在[[Prototype]]链上层存在名为foo的普通数据访问属性,该属性是的非只读(writalbe)时,那就会直接在myObject中添加一个名为foo的新属性,它是屏蔽属性。 2.如果在[[]Prototype]链上层存在foo,但是它被标记为只读(writable)时,那么无法修改已有属性或者在myObject上创建屏蔽属性。非严格模式下该赋值语句会被忽略,严格模式下会抛出一个错误。当然这主要是为了模拟类属性的继承。 3.如果在[[Prototype]]链上层存在foo并且它是一个setter,那就一个会调用这个setter,foo不会添加到(或者说屏蔽于)muObject,也不会重新定义foo这个setter

第二种与第三种情况下也可以屏蔽foo,但是不能用=操作符来赋值,而是用Object.defineProperty(..)来向myObject添加foo。
2.继承 a.instanceof函数 使用instanceof从"类"角度判断a的"祖先"(委托关联):
function Foo() { }; var a = new Foo(); console.log(a instanceof Foo); // 这里使用bind函数,并不会有任何影响,new对this的绑定优先级高于bind函数对this的绑定 var l = { a: 10 }; function Foo() { } var a = new (Foo.bind(l)); console.log(a) console.log(a instanceof Foo);

instanceof在这里回答了:在a的整条[[Prototype]]链中是否有指向Foo.prototype的对象?
b.isPrototypeOf函数 使用isPrototypeOf直接判断一个对象是否在另一个对象的原型链上:
var myObject = {}; var anotherObject = {}; Object.setPrototypeOf(myObject, anotherObject); console.log(anotherObject.isPrototypeOf(myObject));

c.__proto__实现 在对大多数浏览器中也支持一个种非标准的方法来访问内部的[[Protoype]]属性,功能与Object.getPrototypeOf(..),它就是__proto__属性,它并不能存在于你正在使用的对象中,而是和他其常用函数(.toString(..)、isPrototypeOf(..),等等)一样,存在于内置的Object.prototype中。__proto__的实现大致如下:
Object.defineProperty(Object.prototype, "_proto_", { get: function () { return Object.getPrototypeOf(this); }, set: function (value) { Object.setPrototypeOf(this, value); } }) console.log({}._proto_);

d.create使用 Object.create(..)是在ES5中新增的函数,在ES5之前环境中使用,就需要一段简单的polyfill代码,它部分实现了Object.create(..)的功能:
Object.create = function(obj){ function F(){} F.prototype = obj; return new F(); }

这里polyfill的函数,传入null时,并不能创建一个"比较空的对空对象",且真正的Object.create(..)函数的第二个参数可一个定义新对象包含的属性及属性描述符。使用:
console.log(Object.create(null)); // 适用于用来装数据,屏蔽原型链的干扰var anotherObject = {}; var myObject = Object.create(anotherObject, { "name": { value: "Nicholas", writable: true, enumerable: true, configurable: true }, "age": { value: 29, writable: true, enumerable: true, configurable: true } }); console.log(myObject);

e.行为委托 需求:通用控件行为的父类(如Widget)和继承父类的特殊控件子类(如Button)
在JS中下实现类风格代码:
控件"类"// 父类 function Widget(width,height){ this.with = width || 50; this.height = height || 50; this.$elem = null; } Widget.prototype.render = function($where){ if(this.$elem){ this.$elem.css({ width:this.width + "px", height:this.height + "px" }).appendTo($where); } }; // 子类 function Button(width,height,label){ Widget.call(this,width,height); this.label = label || "Default"; this.$elem = $('

使用对象关联风格委托来实现Wight/Button:
// 委托控件对象 var Widget = { init: function (width, height) { this.width = width || 50; this.height = height || 50; this.$elem = null; }, insert: function ($where) { if (this.$elem) { this.$elem.css({ width: this.width + "px", height: this.height + "px" }).appendTo($where); } } }var Button = Object.create(Widget); Button.setup = function (width, height, label) { // 委托调用 this.init(width, height); this.label = label || "Default"; this.$elem = $('

    推荐阅读