你不知道的javascript 上卷

第一部分 作用域和闭包

第一章 作用域是什么

所有编程语言最基本的能力之一就是可以储存变量的值,并且能对这个值进行访问或修改,这种能力在程序中称之为状态
而作用域就是一套用来存储变量,可以方便地找到这些变量的规则

1.1 编译原理

传统编程语言如C++的流程汇总一般会经历三个步骤,统称为'编译'
1.分词/词法分析:
2.解析/语法分析
3.代码生成
而javascript虽然是一门动态的,解释型的编程语言,编译过程却复杂的多; js不同于传统编程语言,编译过程不是发生在构建之前而是发生在代码执行前的几微妙(甚至更短)
简单来说 任何js的代码片段在执行前都会进行编译 然后做好执行它的准备并且通常马上就会执行它

1.2 理解作用域

理解作用域就是理解作用域在js工作原理中所扮演的角色及其作用
举例,当执行var a = 2时 会有以下三个角色进入工作状态

  • 引擎: 从头到尾负责整个js程序的编译和执行过程
  • 编译器: 引擎的好朋友之一 负责语法分析以及代码生成
  • 作用域: 引起的另一个好朋友 负责收集并维护由所有声明的变量组成的一系列查询 并实施一套非常严格的规则 确定当前执行的代码对这些变量的访问权限
    那么这三个角色是任何协同工作的?
  1. 编译器首先会将这段程序分解成词法单元,然后将词法单元解析成一个树结构,在进行代码生成时,编译器会询问作用域是否已经有一个名称为a的变量存在于同一个作用域的集合中. 如果是,编译器会忽略该声明,继续进行编译工作; 否则编译器会要求作用域在当前作用域的集合中声明一个新的变量命名为a(这就是隐式全局变量);
  2. 接下来编译器继续进行代码生成的工作 即a=2这个赋值操作.引擎运行时会首先询问作用域在当前的作用域集合中是否存在一个a变量,如果是,引擎使用这个变量;如果否,引擎会继续查找该变量(作用域链)
  3. 如果引擎最终找到了a变量就会将2赋值给a; 否则引擎会抛出异常
    总结: 变量的赋值操作会涉及到: 编译器在作用域中声明变量,引擎运行时查找变量并进行赋值

1.3 作用域嵌套

作用域是根据名称查找变量的一套规则,通常不会只有一个作用域
当一个块嵌套在另一个块或者一个函数嵌套在另一个函数中就发生了作用域的嵌套,就会形成作用域链,即当前作用域中无法找到的某个变量,引擎会在外层的作用域中继续查找,直到找到或者抵达了全局作用域为止

LHS和RHS查询
引擎在作用域中查找变量的时候分为两种查找 一种是LHS 一种是RHS
赋值操作的目标是谁(LHS) 谁是赋值操作的源头(RHS)
RHS在作用域链中找不到变量是会抛出异常的 ; 而LHS不会抛异常 而是建了一个隐式全局变量(严格模式除外)

console.log(a) //RHS
a = 2 //LHS

function foo(a){
console.log(a)
}
foo(2) //既有LHS也有RHS foo函数的调用需要对foo进行RHS引用 console.log(a)对a进行了RHS 然后a=2对a进行了LHS

function foo(a){
 var b = a;
 return a + b;
}
var c = foo(2); //三处LHS查询: var c = foo(2)对c进行了LHS; var b =a 对b进行了LHS; a=2对a进行了LHS
                //四处RHS查询: var c = foo(2)对foo进行了RHS; var b = a 对b进行了RHS; return a + b 对a和b进行了RHS

总结

作用域是一套规则,用于确定在何处以及如何查找变量,如果查找的目的是对变量进行赋值 就会使用LHS查询; 如果是获取变量的值 则是RHS查询
LHS和RHS都会在当前执行作用域中开始,如有需求会往上一层作用域查找最后抵达全局作用域
不成功的RHS会抛referenceerror异常; 不成功的LHS会创建隐式全局变量(非严格模式下)

第二章 词法作用域

javascript只有词法作用域 又叫做静态作用域;

2.1 词法阶段

编译器的第一个工作阶段叫做词法阶段 也叫作词法化,即对源代码中的字符进行检查
词法作用域是在词法阶段就已经定义好了 即写代码时将变量,函数,或者块作用域写在哪里 作用域就在哪里 不会变了 所以又叫静态作用域

遮蔽效应
在多层嵌套的作用域中如果有同名的标识符 作用域查找的时候会找到最近的 并且停止 即内部的变量遮蔽了外部的变量 叫做遮蔽效应
如果全局变量和内部变量重名 引擎优先找到了内部变量; 如果想要使用全局变量 直接通过window进行索引 比如window.a window.name ...

2.2 欺骗词法

词法作用域由写代码期间函数所声明的位置来定义 但也可以通过一些手段来修改词法作用域 即欺骗词法作用域; 通常不会这么去做 因为欺骗词法作用域会导致性能下降

  • eval: eval函数接受字符串为参数 调用eval会将参数当做函数体去执行,即可以动态的插入一段代码在eval调用的位置; 此时作用域和代码或者函数声明的位置无关而是和eval调用的位置有关了
  • with

总结

词法作用域即静态作用域 意味着作用域是由书写代码时函数声明的位置来决定的
编译器的词法分析阶段能够知道全部变量是在哪里以及如何声明的 从而能够预测在执行过程中如何对它们进行查找
js中有两个机制可以欺骗词法作用域 一个是eval 一个是with 都不建议使用 因为不安全 以及会对性能有所影响

第三章 函数作用域和块作用域

3.1 函数中的的作用域

函数作用域的含义是 属于这个函数的全部变量可以在整个函数的作用域范围内使用及复用 但是函数外无法访问到

3.2 隐藏内部实现

将函数或者对象的封装 不仅可以复用,还可以将具体内容私有化,规避冲突 是良好软件设计的基础

3.3 函数

函数声明和函数表达式
区分函数声明和函数表达式最简单的方法就是看function关键字出现在声明中的位置 如果function是声明中的第一个词 那么就是函数声明 否则就是一个函数表达式
function foo(){} 是函数声明 因为function出现是第一个词
var foo = function(){} 是函数表达式 因为function不是第一个词
函数声明和函数表达式之间最重要的区别是他们的名称标识符将会绑定在何处 比如(function foo(){})() 作用函数表大会 foo只能在内部被访问 外部作用域不行
匿名和具名
没有名称标识符的函数表达式叫做匿名函数 setTimeout(function(){ ...},1000) 定时器中的函数就是匿名函数
有名称标识符的函数叫做具名函数
如果没有函数名 代码可读性会比较差 也不方便引用自身
立即执行函数
IIFE 即立即执行函数的缩写 函数被包含在一对()括号内部 在末尾加上另一个()可以立即执行这个函数

3.4 块作用域

ES6之前js没有块作用域 ES6js有了块作用域的概念

for(var i=0;i<10;i++){
  setTimeout(()=>{console.log(i)},1000) 由于ES5之前没有块作用域 i会被绑定在外部作用域(函数或全局)中 所以此时打印的i是10
}
for(let i=0;i<10;i++){
  setTimeout(()=>{console.log(i)},1000)  es6中的let使得js有了块级作用域 i不会和外部进行绑定 打印i则为 0,1,2,3,4.....
}

with
with也是 块作用域的一种形式 用with从对象中创建出的作用域仅在with声明中有效

try/catch
trycatch的catch分局也会创建一个块作用域 声明的变量尽在catch内部有效
let/const
let和const是es6提供的一种变量声明方式 let和const声明的变量会绑定到所在的块作用域中

var foo = true;
if(foo){
  let bar = foo*2;   //bar变量拥有只在其声明的这个块作用域中生效
  bar =something(bar); 
  console.log(bar)
}
console.log(bar) //referenceerror

总结

函数作用域是js中最常见的作用域,函数作用域中的变量不能被外部访问 开发者经常利用这一特性设计模块
with,trycatch,let,const 可以使得js拥有块作用域 即{..}内的变量不能被外部访问

第四章 提升

javascript的代码是一行一行执行的 但是在执行前会有变量提升,函数声明提升的动作 这是编译器的工作
引擎在执行代码前 编译器会对代码进行词法解析和代码生成 例如var a = 2; 其实js会将其看成两个声明 一个是var a; 一个是a=2;第一个声明在编译阶段由编译器完成; 第二个声明在执行阶段由引擎完成;
编译器在编译阶段会将函数和变量的声明都提升到当前作用域的最顶部 这个过程就叫做提升 需要注意的是只有声明会被提升,赋值和其他运行逻辑会留在原地等待被执行
函数声明会被提升 但函数表达式不会被提升
函数优先
函数声明和变量声明都会被提升 但是需要注意的是 如何变量声明和函数声明重复了 那么变量的声明会被忽略

总结

var a = 2;会被当做两个单独的声明 一个是var a; 编译阶段编译器的任务 一个是a=2 执行阶段引擎的任务
变量的声明和函数的声明会被提升 发生在编译阶段 由编译器完成; 变量的声明和函数的声明重复时 会忽略变量的声明; 这一过程又叫做预解析
函数声明会被提升,函数表达式不会

第五章 作用域闭包

简单来说 闭包是一个函数内部的函数 函数内部的子函数可以读取函数的局部变量
更深度的说 函数可以在其词法作用域之外执行 即闭包
无论通过何种手段将内部函数传递到所在的词法作用域外,它都会持有对原始定义作用域的引用 无论在何处执行这个函数都会使用闭包这个概念
定时器,事件处理函数,ajax请求,等其他异步或者同步任务中只要使用了回调函数,实际上就是使用了闭包 闭包在js中无处不在
模块
模块其实也是利用的闭包的强大威力
模块具备两个必要条件:

  1. 必须有外部的封闭函数,该函数至少被调用一次 每次调用都会创建一个新的模块实例
  2. 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包 并且可以访问或者修改私有的状态

总结

当函数可以记住并访问所在的词法作用域 即使是函数是在当前词法作用域之外执行的 这时就产生了闭包

第二部分 this和对象原型

第一章 关于this

this是js的一个关键字 自动被定义在所有函数的作用域中 代表上下文对象
this既不是指向函数自身 也不是指向函数对象 也不是指向函数的词法作用域

function foo(num) {
console.log( "foo: " + num );
// 记录foo 被调用的次数
this.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
if (i > 5) {
foo( i );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
console.log( foo.count ); // 0 

this是在运行时进行绑定的 不是在函数声明时绑定的 this的指向取决于函数的调用 即this的指向和函数声明没关系 和函数的调用有关

第二章 this全面解析

this的指向是在调用时绑定的 完全取决于函数的调用位置

2.1 调用位置

调用位置就是函数在代码中被调用的位置(不是声明的位置)

2.2 绑定规则

  1. 默认绑定 即函数调用模式 fn(); 直接调用this指向window; 严格模式下 this绑定undefined
  2. 隐式绑定 即方法调用模式 obj.fn();在方法调用模式中,this指向调用当前方法的对象 点语法 中括号语法都属于方法调用模式
    易错题 判断是默认绑定还是隐式绑定
function foo(){ console.log(this.a)};
var obj = {a:2,foo:foo};
var bar = obj.foo;
var a = 'oops,global';
bar(); //你以为是方法调用模式 打印2?  其实是函数调用模式 打印'oops,global'!!!
解析:
var bar = obj.foo 虽然bar是obj.foo的一个引用 但是实际上 引用的是foo函数本身 所以bar()是函数调用模式 不是方法调用模式 this指向window
如果是obj.foo 则是方法调用模式 this指向obj

function foo(){ console.log(this.a)};
function doFoo(fn){fn()};
var obj = {a:2,foo:foo};
var a = 'oops,global';
doFoo(obj.foo); //依然是函数调用模式 打印'oops,global'
解析:
fn其实引用的是foo 
  1. new绑定 即构造函数调用模式 var p=new Person(); new让构造函数中的this指向实例p
  2. 显示绑定 即上下文调用模式 通过call apply bind 来绑定this 想让this指向谁 this就指向谁

2.3 优先级

new绑定>上下文调用>方法调用>函数调用

2.4 绑定例外

如果使用call apply bind的时候传入null或者undefined 则在调用的时候会被忽略 实际this还是函数调用模式 this指向windiw

总结

绑定一个运行中函数的this绑定 就需要找到这个函数的直接调用位置 然后通过规则判断this指向

  1. new 调用 this指向新创建的对象
  2. call apply bind绑定 this指向除undefined和null之外传入的对象
  3. 方法调用模式 this指向对象
  4. 函数调用模式 this指向window;严格模式下this指向undefined
  5. 箭头函数没有this 会继承外层函数调用时的this

第三章 对象

3.1 语法

对象可以通过两种形式定义
第一种 字面量的形式 var obj = {key:value};
第二种 构造函数的形式 var obj = new Object() obj.key=value;
区别在于字面量创建对象可以添加多个键值对 而构造函数形式必须逐个添加属性

3.2 类型

对象是js的基础 在js中一共有6种主要类型

  • string
  • number
  • boolean
  • null
  • undefined
  • symbol
  • bigInt
  • object(array,function是object的一种类型)
    内置对象
    js提供的一些内置构造函数 通过new调用 可以生产一个对应类型的新对象
  • String
  • Number
  • Boolean
  • Obejct
  • Function
  • Array
  • Date
  • RegExp
  • Error

3.3 内容

在对象中 属性名永远都是字符串 如果你使用字符串以外的值作为属性名 js会自动将其转换成字符串
动态属性名
ES6增加的动态属性名 通过[]包裹一个表达式来作为属性名

var obj = {['a'+'b']:'hello'}

数组
数组也是对象 虽然每个下标都是整数 但仍然可以个数组添加属性

var arr = ['foo',42,'bar'];
arr.baz = 'baz';
arr.length = 3
arr.baz = 'baz'
解析:
给数组添加属性 没有改变数组的长度 但是属性确实添加到数组身上
但如果添加的属性 其属性名看起来是个数字 那么就会变成数组的下标
var arr = ['foo',42,'bar'];
arr['3']= 'baz';
arr.length = 4
arr[3]= 'baz'

复制对象
深拷贝 var newObj = JSON.parse(JSON.stringify(someObj));
浅拷贝 var newObj = Object.assign({},someObj)
属性描述符
ES5之前 js语言本身没有提供可以直接检测属性特性的方法 比如判断属性是否是只读
ES5开始 属性具备了属性描述符 Object.getOwnPropertyDescriptor(obj,属性) 可以查看对象属性对应的属性描述 比如writable enumerable configurable
ES5的Object.defineProperty 可以给对象添加一个新属性或者修改已有属性的特性
configurable如果设置成false 是单向操作 无法再将其改成true 也导致无法删除这个属性 无法将writable由false改成true
对象的不可变
以下几种操作可以实现对象不可改变

  1. writable和configurable为false 不可修改 重定义或删除
  2. Object.preventExtensions 禁止对象新添加属性
  3. Object.seal 会创建一个密封的对象 不能添加新属性 不能重新配置或者删除任何现有属性 但可以修改现有属性的值
  4. Object.freeze 创建一个冻结的对象 禁止对对象本身及其任意直接属性的修改
    getter和setter
    get和set一般是成对设置
var myObject = {
// 给 a 定义一个getter
get a() {
return this._a_;
},
// 给 a 定义一个setter
set a(val) {
this._a_ = val * 2;
}
};
myObject.a = 2;
myObject.a; // 4

存在性
in操作符 可以判断对象是否存在某个属性
hasOwnProperty 可以判断某个属性是否是对象自身的 还是继承的
propertyIsEnumerable(..) 判断属性是否可以枚举

第四章 混合对象'类'

面向类的设计模式: 实例化,继承,多态
面向对象编程强调的是数据和操作数据的行为进行绑定(封装)成类
js是面向过程的编程语言 并不是面向对象的编程语言但是js可以模拟面向对象 比如ES6提供的class 有近似类的语法 本质是原型的语法糖

第五章 原型

js中每个对象都有prototype属性指向原型 当在进行属性查找时 如果自身没有 就会到原型对象上面找 直到找到属性或者查找完整条原型链
原型链的顶端指向Object.prototype
属性设置和屏蔽
通常情况下在一个对象上面查询某个属性 这个对象上没有 会查询其原型链
如果给一个对象增加一个属性 且原型链上有同名的属性 则会在这个对象上增加一个屏蔽属性 再次get的时候 获取的是对象上的而非原型链上的
obj.foo='bar'会出现的三种情况

  1. 如果原型链上存在同名foo属性 并且没有标记为只读 那么会直接在obj上添加一个foo新属性 即屏蔽属性
  2. 如果原型链存在foo 且被标记为只读 那么无法修改已有属性 也无法在obj上新创建一个foo屏幕属性 非严格模式下这条赋值语句被忽略; 严格模式下报错
  3. 如果原型链存在foo 且是一个setter 那么就会调用这个setter 但是obj上不会新创建一个foo屏蔽属性
    继承
    继承是指javascript会在两个对象之间创建一个关联,这样一个对象就可以通过委托访问另一个对象的属性和函数

第六章 行为委托

附录A ES6中的class

原文地址:https://www.cnblogs.com/hanyan99/p/14417161.html