《你不知道的JavaScript上卷》

150 阅读13分钟

第三章

函数作用域和块作用域

  • 隐藏内部实现
  • 规避冲突
function doSometing(a){
  b = a + doSometingElse(a*2);
  console.log(b*3)
}
function doSomethingElse(a){
  return a - 1
}
var b
doSomething(2)


function doSomething(a){
  function doSometingElse(a){
    return a -1
  }
  var b = a + doSometingElse(a * 2)
  console.log(b*3)
}
doSomething(2) // 15

如何设计内部更加的私有化,内聚

  • 当程序中加载了多个第三方库时,如果他们没有妥善地将内部私有函数或变量隐藏起来,就会很容易引发冲突。
  • 因此通常这些库会暴露一个对象
var MyReallyCoolLibrary = {
  awesome:'stuff',
  doSomething:function(){
  }
  doAnotherThing:function(){
  }
}
  • 模块管理

函数作用域

我们已经知道,在任意代码片段外部添加包装函数,可以将内部的变量和函数定义"隐藏"起来,外部作用域无法访问包装函数内部的任何内容

函数声明和函数表达式之间最重要的区别是它们的名称标识符将会绑定在何处

  • 立即执行函数

匿名和具名

对于函数表达式最熟悉的可能就是回调函数了

setTimeout(function(){ console.log("匿名函数表达式") },0)

函数表达式可以是匿名的,而函数声明则不可以省略函数名

始终给函数表达式命名是一个最佳实践

setTimeout(function foo(){ console.log('具名函数' )},0)

立即执行函数表达式

// 具名
var a = 2;
(function IIFE(){
  var a = 3;
  console.log(a) // 3
})()
console.log(a) //2
// 匿名
(function(){}())
  • 进阶用法
var a = 2
(function IIFE(global){
  var a = 3
  console.log(a) //3
  console.log(global.a) //2
})(window)

总结

函数是JavaScript中最常见的作用域单元。本质上,声明在一个函数内部的变量或函数会在所处的作用域中“隐藏”起来,这是有意为之的良好软件的设计原则

从开始到现在都是在讲作用域的概念

第四章

提升

直觉上认为JavaScript代码在执行时是由上到下一行一行执行的。但实际这并不完全正确,

a = 2
var a
console.log(a)

// 输出的结果是2
console.log(a)
var a = 2 

编译阶段中的一部分工作就是找到所有的声明,并用合适的作用域将它们关联起来。

包括变量和函数在内的所有声明都会在任何代码被执行之前首先被处理

var a = 2 =>>> var a;a = 2;

因此,打个比方,这个过程就好像变量和函数声明从它们在代码中出现的位置被“移动到了最上面”这个过程叫作提升

函数声明会被提升,但是函数表达式却不会被提升

第五章

作用域闭包

function foo(){
  var a = 2;
  function bar(){
    console.log(a)
  }
  return bar
}
var baz = foo()
baz() // 2 --朋友,这就是闭包的效果

bar()依然持有对该作用域的引用,而这个引用就叫作闭包

function foo(){
  var a = 2
  function baz(){
    console.log(a)
  }
  bar(baz)
}
function bar(fn){
  fn()
}
foo()
var fn
function foo(){
  var a = 2
  function baz(){
    console.log(a)
  }
  fn = baz
}
function bar(){
  fn()
}
foo()
bar() //2

无论通过任何手段间内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包

在定时器,事件监听器,Ajax请求,跨窗口通信,Web Workers 或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包

循环和闭包

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

延迟函数的回调会在循环结束才执行。 在调用之前会一直保存的变量的作用域,当执行的时候会去查找该变量

  • 所以问题还是出现在了作用域的引用,只要我们每次嵌套作用域,那么我么引用的东西就不是同一个变量了
for(var i=1;i<=5;i++){
  (function(){
    var j = i;
    serTimeout(function timer(){ console.log(j)},j*1000)
  })()
}
// 改进
for(var i=1;i<=5;i++){
  (function timer(j){
    setTimeout(function timer(){
      console.log(j)
    },j*1000)
  })(i)
}

在迭代内使用IIFE会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问

for循环头部会有一个特殊的行为。这个行为指出变量在循环过程中不止被声明一次,每次迭代都会声明。随后每个迭代都会使用上一个迭代结束时的值来初始化这个变量

for(let i = 0;i<=5;i++){
 setTimeout(function(){console.log(i)},i*1000)
}

模块

模块模式具备两个条件:

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

实现简单的单例模式

var foo = (function CoolModule(){
  var someting = "cool"
  var another = [1,2,3]
  function doSomething(){
    console.log(something)
  }
  funtion doAnother(){
    console.log(another.join("!"))
  }
  return {
   doSomething:doSomething,
   doAnother:doAnother
  }
 })()
 foo.doSomething //cool
 foo.doAnother // 1 ! 2 ! 3

接收参数

function CoolModule(id) {
  function identify() {
    console.log(id)
  }
  return {
    identify: identify
  }
}
var foo1 = CoolModule("foo 1")
var foo2 = CoolModule("foo 2")
foo1.identify()
foo2.identify()
var foo = (fucntion CoolModule(id){
  function change(){
    publicAPI.identigy = identigy2;
  }
  function identigy1(){
    console.log(id)
  }
  function identigy2(){
    console.log(id.toUpperCase())
  }
  var publicAPI = {
    change:change,
    identigy:identigy1
  }
  return publicAPI
})("foo module")

foo.identigy(); // foomodule
foo.change();
foo.identigy(); // FOO MODULE

小结

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

作用域和闭包真没这么简单 ==>>>> 模块

附录A 动态作用域

词法作用域是静态的,因此这里输出的是2。静态词法作用域,是根据声明的地方调用作用域。 动态作用域并不关心函数和作用域是如何声明以及在何处声明的,只关心他们从何处调用。

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

如果是动态作用域那么 结果应该是3

this机制某种程度上很像动态作用域

第二部分

第一章

关于this

this到底是什么

之前我们说过this是运行时进行绑定的,并不是编写时绑定,它的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式

当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈),函数的调用方式,传入的参数等信息。this就是这个记录的一个属性,会在函数执行的过程中用到

调用位置

调用栈和调用位置

function baz(){
  // 当前调用栈是:bar
  // 因此,当前调用位置时全局作用域
  console.log("baz")
  bar() // <-- bar的调用位置
}
function bar(){
  // 当前调用栈是 baz -> bar
  // 因此,当前调用位置在baz中
  console.log("bar")
  foo(); // <-- foo的调用位置
}

function foo(){
    // 当前调用栈是 baz -> bar -->foo
  // 因此,当前调用位置在bar中
  console.log("bar")
  
}

默认绑定

  • 如果使用严格模式(strict mode)则不能将全局对象用于默认绑定,因此this会绑定到undefined
function foo(){
  "use stirct"
  console.log(this.a)
}
var a =2 
foo() //TypeError:this is undefined

隐式绑定

隐式绑定的函数会丢失绑定对象

funciton foo(){
  console.log(this.a)
}
var obj = {
  a:2,
  foo:foo
}
var bar = obj.foo
var a = "oops,global"
bar()

它引用foo函数本身,因此此时的bar()其实是一个不带任何修饰的函数调用,因此应用了默认绑定

显示绑定

call() apply() 这两个方法是如何工作的呢?它们的第一个参数是一个对象,是给this准备的,接着在调用函数时将其绑定到this.

API 调用的“上下文”

许多内置函数,都提供了一个可选的参数,通常被称为“上下文”

function foo(el) {
  console.log(el, this.id)
}
var obj = {
  id: 'awesome'
}
// 调用foo()时把this绑定到obj
const arr = [1, 2, 3]
arr.forEach(foo, obj);

new绑定

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

使用new 来调用foo(..)时,我们会构造一个新对象并把它绑定到foo()调用中的this上。

被忽略的this

如果你把null或者undefined作为this的绑定对象传入call,apply,或者bind。

function foo(){
  console.log(this.a)
}
var a = 2
foo.call(null)
  • 什么情况下传入null 一种常见的做法是使用apply()来“展开一个数组”,并当做参数传入一个函数。类似地,bind()可以对参数进行柯里化(预先设置一些参数)
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
 function foo(a,b){
   console.log(a,b)
 }
 // 我们的DMZ空对象
 var ø =Object.create(null)
 // 把数组展开成参数
 foo.apply(ø,[2,3])
 // 使用bind() 进行柯里化
 var bar = foo.bind(ø,2)
 bar(3) 

this词法

箭头函数是根据外层(函数或全局)作用域来决定this.

箭头函数的绑定是无法被修改的

function foo(){
  // 返回一个箭头函数
  return (a) => {
    // this继承来自foo()
    console.log(this.a)
  }
}
var obje1 = {
 a:2
}
var obje2 = {
 a:3
}
var bar = foo.call(oje1)
bar.call(obje2) //2 不是3

foo()内部创建的箭头函数会捕获调用时foo()的this。由于foo()的this绑定到obj1,箭头函数的this也会绑定到obj1,箭头函数的绑定无法被修改

第三章

对象

前面中我们介绍了函数调用位置的不同会造成this绑定对象的不同。但是对象到底是什么,为什么我们需要绑定它们

内置对象

JavaScript 中还有一些对象子类型,通常被称为内置对象。有些内置对象的名字看起来和简单基础类型一样,不过实际上它们的关系更复杂

  • String
  • Number
  • Boolean
  • Object
  • Function
  • Array
  • Date
  • RegExp
  • Error

类型

  • string
  • number
  • boolean
  • null
  • undefined
  • object
var strPrimitive = "I am a string"
typeof strPrimitive // "string"
strPrimitive instanceof String // false

var strObject = new String("I am string")
typeof strObject // "object"
strObject instanceof String // true

思考

var strPrimitive = "I am a string"
console.log( strPrimitive.length ) //13
console.log( strPrimitive.chatAt(3)) // "m"

我们直接字符串字面量上访问属性或者方法,之所以可以这样做,是因为引擎自动把字面量转换成String对象,

同样的数值字面量上也是 42.359.toFixed(2)

null和undefined 没有对应的构造形式,它们只有文字形式。相反,Date只有构造,没有文字形式

对于Object,Array,Function和RegExp(正则表达式)来说,无论使用文字形式还是构造形式,它们都是对象,不是字面量。

内容

var myObject = {
  a:2
}
myObject.a //2
myObject["a"] //2

引用名称为 “Super-Fun” 必须要用["Super-Fun"]

var myObject = {
  a:2
}
var idx
if(wanA){
  idx = "a"
}
console.log(myObject[idx])  //2

在对象中,属性名永远都是字符串

如果你使用string(字面量)以外的其他值作为属性名,那它首先会被转换为一个字符串。

var myObject = {}
myObject[true] = "foo"
myObject[3] = "bar"
myObject[myObject] = "baz"

myObject["true"] // "foo"
myObject["3"] // "bar"
myObject["[object object]"] // "baz"

可计算属性名

Es6 增加了可计算属性名

var myObject = {
  [prefix + "bar"]:"hello",
  [prefix + "baz"]:"world"
}
myObject["foobar"] // hello
myObject["foobaz"] // world

最常用的场景可能是ES6的符号(Symbol)

var myObject = {
  [Symbol.Something]:"hello world"
}

属性描述符

我们使用Object.defineProper()

var myObject = {}
Object.defineProperty(myObject,"a",{
  value:2,
  writable:true,
  configurable:true,
  enumerable:true
})

存在性

判断对象中是否存在这个属性

var myObject = {
  a:2
}
("a" in myObject) //true
("b" in myObject) //fasle

myObject.hasOwnProperty("a") // true
myObject.hasOwnProperty("b") // false

for of

for...of 循环首先会向被访问对象请求一个迭代器对象,然后通过迭代器对象的next()方法来遍历所有返回值

我们使用内置的@@iterator来手动遍历数组,看它是怎么工作的

var myArray = [1,2,3]
var it = myArray[Symbol.iterator]()

it.next() //{value:1,done:false}
it.next() //{value:2,done:false}
it.next() //{value:3,done:false}
it.next() // {done:true}

小结

许多人都以为“JavaScript中万物都是对象”,这是错误的。对象是6个(或者7个,取决于你的观点)基础类型之一。对象有包括function在内的子类型,不同子类型具有不同的行为,比如内部标签[object Array]表示这是对象的子类型数组

属性的特征

第四章

混合对象“类”

面向类的设计模式:实例化(instantiation),继承(inheritance),和相对多态(polymorphism)

这些概念实际上无法直接对应到JavaScript的对象机制,因此我们会介绍许多JavaScript开发者所使用的的解决方法(比如混入,miixn)

第五章

原型

JavaScript中的对象有一个特殊的[[Prototype]]内置属性,其实就是对于其他对象的引用。几乎所有的对象在创建时[[Prototype]]属性都会被赋予一个非空值 思考

var myObject = {
  a:2
};
myObject.a //2

第一步是检查对象本省是否有这个属性,如果有的话就使用它。但是如果a不在myObject中,就需要使用对象的[[Prototype]]链了

var anotherObject = {
  a:2
}
// 创建一个关联到anotherObject 的对象
var myObject = Object.create(anotherObject)
myObject.a //2

Object.prototype

所有普通的[[Prototype]]链最终都会指向内置的Object.prototype.

隐式屏蔽

var anotherObject = {
  a:2
}
var myObject = Object.create(anotherObject)
anoterObject.a //2
myObject.a //2

anotherObject.hasOwnProperty("a") //true
myObject.hasOwnProperty("a") //false

myObject.a++ // 隐式屏蔽
anoterObject.a //2
myObject.a //3
myObject.hasOwnProperty("a") // true

"类"

在JavaScript中,类无法描述对象的行为,(因为根本就不存在类)对象直接定义自己的行为。再说一遍,JavaScript中只有对象

关于名称

在JavaScript中,我们并不会将一个对象("类")复制到另一个对象(”实例“),只是将它们关联起来。

到底是什么让我们认为Foo是一个”类“呢?

  • 其中一个原因就是我们看到了关键字new
  • Foo() 的调用方式很像初始化类时类构造函数的调用方式
function Foo() {
  
}
Foo.prototype.constructor === Foo  //true
var a = new Foo()
a.constructor === Foo //true
function NothingSpecial(){
  console.log("Don`t mind me")
}
var a = new NothingSpecial()
// Don`t mind me
a; // {}

NothingSpecial 只是一个普通的函数,但是使用new调用时,它就会构造一个对象并赋值给a, 换句话说,在JavaScript中对于”构造函数“最准确的解释是,所有带new的函数调用

函数不是构造函数,但是当且仅当使用new时,函数调用会变成”构造函数调用“

技术

function Foo(name){
  this.name = name
}
Foo.prototype.myName = function(){
  return this.name
}
var a = new Foo("a")
var b = new Foo("b")

因此,在创建过程中,a和b 的内部[[Prototype]]都会关联到Foo.prototype上。当a和b中无法找到myName时,它会(通过委托)在Foo.prototype上找到

a1.constructor是一个非常不可靠并且不安全的引用。通常来说要尽量避免使用这些引用

原型继承

function Foo(name){
  this.name = name
}
Foo.prototype.myName = function(){
  return this.name
}
function Bar(name,label){
  Foo.call(this,name)
  this.label = label
}
// 我们创建了一个新的Bar.prototype对象并关联到Foo.prototype
Bar.prototype = Object.create(Foo.prototype)
// 注意! 现在没有Bar.prototype.constructor了
// 如果你需要这个属性的话可能需要手动修复一下它
Bar.prototype.myLable = function(){
  return this.myLable
}
var a = new Bar("a","obj a")
a.myName() // a
a.myLable() //obj a

这段代码的核心部分就是语句Bar.prototype = Object.create(Foo.prototype)。调用Object.create()会凭空创建一个”新对象“并把新对象内部的[[Prototype]]关联到你指定的对象(本列中是Foo.prototype)

换句话说,这条语句的意思是:”创建一个新的Bar.prototype对象并把它关联到Foo.prototype“

常见错误的做法,但是也可以达到目的

// 和你想要的机制不一样
Bar.prototype = Foo.prototype

//基本上满足你的需求,但是可能会产生不一样的副作用
Bar.prototype = new Foo()

要注意的关联和引用的区别 Bar.prototype = Foo.prototype 并不会创建一个关联到Bar.prototpye的新对象,它只是让Bar.prototype直接引用Foo.prototype对象。因此当你执行类似Bar.prototype.myLable = ... 的赋值语句时会直接修改Foo.prototype对象本身。

Es6添加了辅助函数Object.setPrototypeOf(),可以用标准并且可靠的方法来修改关联。

Bar.prototype = Object.create(Foo.prototype)

Object.setPrototypeOf(Bar.prototype,Foo.prototype)

原型链

[[Prototype]]机制就是存在于对象的一个内部链接,它会引用其他对象

这个链接的作用:如果是对象上没有找到需要的属性或者方法引用,引擎就会继续在[[Prototype]]关联上进行查找。依次类推 --- 这一系列对象的链接被称为”原型链“

第六章

行为委托

比较思维

下面典型的(”原型“)面向对象风格

function Foo(who){
  this.me = who
}
Foo.prototype.identify = function(){
  return "I am" + this.me
}
function Bar(who){
  Foo.call(this,who)
}
Bar.prototype = Object.create(Foo.prototype)
Bar.prototpye.speak = functioni(){
  alert("Hello" + this.identify() + ".")
}
var b1 = new Bar("b1")
var b2 = new Bar("b2")

b1.speak()
b2.speak()

对象关联风格来编写

Foo = {
  init:function(who){
    this.me = who
  },
  identify:function(){
    return "I am" + this.me
  }
}
Bar = Object.create(Foo)
Bar.speak = function(){
  alert("Hello" + this.identify())
}
var b1 = Object.create(Bar)
b1.init("b1")
var b2 = Object.create(Bar)
b2.init("b2")

b1.speak()
b2.speak()

JavaScript中的函数之所以可以访问call(),apply(),bind(),就是因为函数本身是对象。而函数对象同样有[[Prototype]]属性并且关联到Function.prototype对象,因此所有函数对象都可以通过委托调用这些默认方法。

总结

  • 更深入了作用域和闭包的内容还有对this的使用
  • 但是在讲对象原型的时候是另一种方式,对象关联的方式来讲。在本书中他更推荐的是使用对象关联的放来创建面向对象。而不是我们去模拟类的方式创建面向对象。自己对这方面也还没有足够的了解,所以本书会先到这一段。也是为了加深自己的印象,别人的思想不可能这么快就理解的。多敲代码吧!

---吃不了自律的苦,就要受平庸的罪。