你不知道的JavaScript(上) - 阅读笔记

你不知道的JavaScript(上)


① 做用域和闭包

一. 做用域是什么?

做用域是一套规则,用于在何处以及如何查找变量(标识符).若是查找的目的是对变量进行赋值,那么就行使用LHS查询;若是目的是获取变量的值,就会使用RHS查询.赋值操做会致使LHS查询. =操做符或调用函数时传入参数的操做都会致使关联做用域的赋值操做.css

  • PS: 对变量赋值LHS,为变量取值RHS

JavaScript引擎首先会在代码执行前对其编译,在这个过程当中,像var a = 2这样的声明被分解成两个独立的步骤:git

  1. 首先,var a在其做用域中声明新变量.这会在最开始的阶段,也就是代码执行前进行.
  2. 接下来,a=2会查询(LHS查询)变量a并对其进行赋值 LHSRHS查询都会在当前执行做用域中开始,若是有须要(没有找到所需的标识符),就会向上级做用域继续查找目标标识符,这样每次上升一级,最后抵达全局做用域,不管找到或没找到都将中止.
  • PS: 把做用域链比喻成一栋建筑

不成功的RHS引用会致使抛出ReferenceError异常. 不成功的LHS引用会致使自动隐式地建立一个全局变量(非严格模式下),该变量使用LHS引用的目标做为标识符,或者抛出ReferenceError(严格模式下)github

对变量赋值LHS,为变量取值RHSchrome

LHS与RHS

对变量赋值`LHS`,为变量取值`RHS`
复制代码

二. 词法做用域

词法做用域意味着做用域是由书写代码时函数声明的位置决定. 编译的词法分析阶段基本可以知道所有标识符在那里以及如何声明的,从而可以预测在执行过程当中如何对它们进行查找.设计模式

JavaScript中有两个机制能够"欺骗"词法做用域: eval(...)with. 前者能够对一段包含一个或多个声明的"代码"字符串进行演算,并借此来修改已存在的词法做用域. 后者本质上是经过一个对象的引用看成做用域来处理,将对象的属性看成做用域中的标识符来处理,从而建立一个新的词法做用域.数组

这两个机制的反作用是引擎没法在编译时对做用域查找进行优化,由于引擎只能谨慎的认为这样的优化是无效的.使用这其中一种机制都将致使代码运行变慢.不要使用它们浏览器

三. 函数做用域和块做用域

函数是JavaScript中最多见的做用域单元. 本质上,声明在一个函数内部的变量或者函数会在所处的做用域中被"隐藏"起来,这是有意为之的良好软件的设计原则安全

但函数不是惟一的做用域单元. 块做用域指的是变量和函数不只能够属于所处的做用域也能够属于某个代码块.babel

  • 从ES3开始,try/catch结构在catch分句中具备块做用域

在ES6中引入了let关键字,用来在任何代码块中声明变量,if(..){let a = 2}会声明一个劫持if{...}块的变量,并将变量添加到这个块中.数据结构

有些人认为块做用域不该该彻底做为函数做用域的替代方案.两种功能应该同时存在,开发者能够而且也应该根据须要选择使何种做用域,创造可读,可维护的优良代码

四. 变量提高

咱们习惯将var a = 2;看做是一个声明,而实际上JS引擎并不认为. 它将var aa=2看成两个单独的声明,第一个是编译阶段的任务,而第二个则是执行阶段的任务

这意味着不管做用域中的声明出如今什么地方,都将在代码自己被执行前首先进行处理,能够将这个过程形象地想象成全部的声明(变量和函数)都会被"移动"到各自做用域的最顶端,这个过程称为提高

声明自己会被提高,而包括函数表达式的赋值在内的赋值操做并不会提高.

要注意避免重复声明,特别是当普通的var声明和函数声明混合在一块儿的时候,不然会引发不少危险的问题!

PS: 函数声明和变量声明都会被提高. 可是一个值得注意的细节(这个细节能够出如今有多个'重复'声明的代码中)是函数会首先被提高,而后才是变量.

foo() //1
var foo
function foo() {
    console.log('1')
}
foo = function() {
    console.log('2')
}

复制代码

五. 闭包

闭包无处不在,你只须要识别并拥抱它

  • 闭包的模型
function foo() {
    var a = 2
    function bar() {
        console.log(a)
    }
    return bar
}
var baz = foo()
baz() //2
复制代码

在这个例子中,它在本身定义的词法做用域之外的地方执行

foo()执行后,一般会期待foo()的整个内部做用域都被销毁,由于咱们知道引擎有垃圾回收器用来释放再也不使用的内存空间. 因为看上去foo()的内容不会再被使用,因此咱们会认为垃圾回收机制会将其回收

而闭包能够阻止这件事的发生. 拜bar()所声明的位置所赐,它拥有涵盖foo()内部做用域的闭包,使得该做用域一直存活,以供bar()在以后任什么时候间进行引用

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

固然,不管使用何种方式对函数类型的值进行传递,当函数在别处被调用时均可以观察到闭包

function foo () {
    var a = 2
    function baz() {
        console.log(a) //2
    }
    bar(baz)
}
function bar(fn) {
    fn() //这就是闭包
}

复制代码
var fn
function foo() {
    var a = 2
    function baz() {
        console.log(a)
    }
    fn = baz // 将baz分配给全局变量
}
function bar() {
    fn() // 这就是闭包
}
foo()
bar() //2
复制代码

不管经过何种手段将内部函数传递到所在词法做用域之外,它都会持有对原始定义做用域的引用,不管在何处执行这个函数都会使用闭包

闭包的场景

PS:定时器中的闭包

function wait(mes) {
    setTimeout(function timer(){
        console.log(mes) //这就是闭包
    },1000)
}
复制代码

解析: 将一个内部函数传递给setTimeout(...). timer具备涵盖wait(...)做用域的闭包,所以还保有对变量message的引用.

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

循环和闭包

PS:典型例子

for(var i = 1;i<=5;i++) {
    setTimeout(function timer(){
        console.log(i) // 每秒一次的频率输出五次6
    },i*1000)
}
复制代码

解析:

  1. 6从哪儿来?

    循环终止条件是i再也不<=5,条件首次成立时i的值是6

  2. 运行机制

    根据做用域的工做原理,实际状况是尽管循环中五个函数是在各个迭代中分别定义的,可是它们都被封闭在一个共享的全局做用域中,所以其实是同一个并仅有一个i

改进:

for(var i=1;i<=5;i++) {
    (function(j){
        setTimeout(function timer(){
            console.log(j)
        },j*1000)
    })(i)
}
复制代码

解析:

在迭代中使用IIFE会为每次迭代都生成一个新的做用域,使得延迟函数的回调能够将新的做用域封闭在每一个迭代内部,每一个迭代中都会含有一个具备正确值的变量提供咱们访问

进一步改进:

for(var i=1;i<=5;i++){
    let j=i // 闭包的块做用域
    setTimeout(function timer(){
    	console.log(j)
    },j*1000)
}
复制代码

终极模式:

for(let i=1;i<=5;i++){
    setTimeout(function timer(){
    	console.log(i)
    },i*1000)
}
复制代码

解析:

let声明变量有一个特殊行为,指的是变量在循环过程当中不止声明一次,每次迭代都会声明. 随后的每一个迭代都会使用上一个迭代结束的值来初始化这个变量

模块

function CoolModule(){
    var something = 'cool'
    var another = [1,2,3]
    function doSomething() {
        console.log(something)
    }
    
    function doAnother() {
        console.log(another.join('!'))
    }
    return {
        doSomething:doSomething,
        doAnother:doAnother
    }
}
复制代码

CoolModule()只是一个函数,必需要经过调用它来建立一个模块实例. 若是不执行外部函数,内部做用域和闭包都没法建立.

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

小结

闭包其实是一个普通且明显的事实,那就是咱们在词法做用域的环境下写代码,而其中的函数也是值,咱们能够开心的传来传去

当函数能够记住并访问所在的词法做用域,即便函数是在当前词法做用域以外执行,这时就产生了闭包

模块有两个主要的特征: (1)为建立内部做用域而调用一个包装函数 (2) 包装函数的返回值必须至少包括一个对内部函数的引用,这样就会建立涵盖整个函数内部做用域的闭包.

六. 附录

动态做用域

动态做用域并不关心函数和做用域是如何声明以及在何处声明的,只关心它们从何处调用. 换句话是,做用域链是基于调用栈,而不是代码中的做用域嵌套.

function foo(){
    console.log(a) // 2 (不是3)
}
function bar() {
    var a = 3
    foo()
}
var a = 2
bar()
复制代码

解析:

事实上JavaScript并不具备动态做用域. 它只有词法做用域,简单明了. 可是this的动态机制某种程度上很像动态做用域

主要区别

词法做用域是在写代码或者定义时肯定的,而动态做用域是在运行时肯定的.(this也是) 词法做用域关注函数在何处声明,而动态做用域关注函数从何处调用.

块做用域的替代方案

foeExample:
{
  let a = 2
  console.log(2) // 2
}
console.log(a) // ReferenceError

// =====> ES6以前
try{
   throw 2
 }catch (a) {
   console.log(2)
 }
 console.log(a) // ReferenceError

复制代码
try/catch性能

问: 为何不直接使用IIFE来建立做用域?

答:

首先,try/catch的性能的确糟糕,但技术层面上没有合理的理由来讲明try/catch必须这么慢,或者会一直这么慢下去. 自从TC39支持在ES6的转换器中使用try/catch后,Traceur团队已经要求chrome对try/catch的性能进行改进,他们显然有很充分的动机来作这件事情

其次: IIFE和try/catch并非等价的,由于若是将一段代码中的任意一部分拿出来用函数进行包裹,会改变这段代码的含义,其中this,return,break和continue都会发生变化. IIFE并非一个普通的解决方案,它只适应在某些状况下进行手动操做

匿名函数没有name标识符,会致使?
  1. 调用栈更难追踪
  2. 自我引用更难
  3. 代码更难理解

this的词法

箭头函数的this

箭头函数在涉及this绑定的行为和普通函数的行为彻底不一致. 它放弃了因此普通this绑定的规则,取而代之的是用当前的词法做用域覆盖了this的值(箭头函数不止于少写代码)

② this和对象原型

一. 关于this

它的做用域

this在任何状况下都不指向函数的词法做用域. 在JavaScript内部,做用域确实和对象相似,可见的标识符都是它的属性. 可是做用域"对象"没法经过JavaScript代码访问,它存在JavaScript引擎内部

每当你想要把this和词法做用域的查找混合使用时,必定要提醒本身,这是没法实现的

this是什么?

this是在运行时进行绑定的,并非在编写时绑定的,它的上下文取决于函数调用时的各类条件. this的绑定和函数的声明的位置没有任何关系,只取决于函数的调用方法

this其实是在函数被调用时发生的绑定,它指向什么彻底取决于函数在哪里被调用

二. this的全面解析

调用位置

调用位置就是函数在代码中被调用的位置

调用栈就是为了到达当前执行位置所调用的因此函数

this绑定规则

1. 默认绑定

独立函数调用

function foo() {
    console.log(this.a)
}
var a = 2
foo() // 2
复制代码

解析: foo()是直接使用不带任何修饰的函数引用进行调用的,所以只能使用默认绑定,没法应用其余规则

若是使用严格模式(strict mode),则不能将全局对象用于默认绑定,所以this会绑定到undefined

function foo() {
 "use strict"
    console.log(this.a)
}
var a =  2
foo() // TypeError: this is undefined
复制代码

这里有一个微妙但很是重要的细节,虽然this的绑定规则彻底取决于调用位置,可是只有foo()运行在非strict mode

下时,默认绑定才能绑定到全局对象;在严格模式下调用foo()则不影响默认绑定.

function foo() {
    console.log(this.a)
}
var a = 2
(function(){
 "use strict"
    foo() //2
})()
复制代码
2.隐式绑定

forexample:

function foo () {
    console.log(this.a)
} 
var obj = {
    a: 2,
    foo:foo
}
obj.foo() //2
复制代码

当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象

例外:

对象属性引用链中只有上一层或者说最后一层在调用位置中起做用.

function foo() {
    console.log(this.a)
}
var obj2 = {
    a:42,
    foo:foo
}
var obj1 = {
    a:2,
    obj2:obj2
}
obj1.obj2.foo() // 42
复制代码
隐式丢失

一个最多见的this绑定的问题就是被隐式绑定的函数丢失绑定对象,也就是说它会应用默认绑定,从而把this绑定到全局对象或者undefined上,取决因而否是严格模式

function foo() {
    console.log(this.a)
}
var obj = {
    a:2,
    foo:foo
}
var bar = obj.foo // 函数别名
var a = 'oops,global'
bar() // oops,global
复制代码

解析:

虽然bar是obj.foo的一个引用,可是实际上,它引用的是foo函数自己,所以此时的bar()实际上是一个不带任何修饰的函数调用,使用的默认绑定

function foo() {
    console.log(this.a)
}
function doFoo(fn){
    // fn其实引用的是foo
    fn(); // <--调用位置
}
var obj = {
    a:2
    foo:foo
}
var a = 'oops,global'
doFoo(obj.foo) // oops,global

复制代码

解析:

参数传递其实就是一种隐式赋值,所以咱们传入函数也会被隐式赋值

  • 回调函数丢失this绑定是很是常见的,接下来学习如何经过固定this来修复这个问题
3.显式绑定

JavaScript提供的绝大多数函数以及咱们本身建立的全部函数均可以使用call(...)apply(...)方法

它们的第一个参数是一个对象,是给this准备的,接着在调用函数时将其绑定到this. 由于你能够直接指定this的绑定对象,所以咱们称之为显示绑定.

3.1 硬绑定

硬绑定是一种很是常见的模式,ES5提供了内置的方法Function.prototype.bind(...)

bind(...)会返回一个硬编码的新函数,它会把你指定的参数设置为this的上下文并调用原始函数

4.new绑定

包括内置函数和自定义函数在内的全部函数均可以用new来调用,这种函数调用被称为构造函数调用. 实际上并不存在所谓的"构造函数",只有对于函数的构造调用

使用new来调用函数,或者说发生构造函数调用时,会自动执行下面的操做

  1. 建立一个全新的对象
  2. 这个对象会被执行[[Prototype]]连接
  3. 这个新对象会绑定到函数调用的this
  4. 若是函数没有返回其余对象,那么new表达式中的函数调用会自动返回这个新对象

判断this(重要!!!)

按照下面的顺序进行判断(记住特例):

  1. 函数是否在new中调用(new 绑定)?若是是的话this绑定的是新建立的对象 var bar = new foo()
  2. 函数是否经过call或apply(显示绑定)或者硬绑定调用? 若是是的话, this绑定的是指定的对象 var bar = foo.call(obj2)
  3. 函数是否在某个上下文对象中调用(隐式绑定)?若是是的话,this绑定的是那个上下文对象 var bar = obj.foo()
  4. 若是都不是的话,使用的是默认绑定. 若是在严格模式下,就绑定到undefined,不然绑定的全局对象 var bar = foo()

不过...凡是都有例外,接下来咱们介绍例外吧

this的绑定例外

  1. 被忽略的this

若是咱们把nullundefined做为this的绑定对象传入call apply 或者bind,这些值在调用时会被忽略,实际应用的是默认绑定

function foo() {
    console.log(this.a)
}
var a = 2
foo.call(null) // 2
复制代码

柯里化传入更安全的this

在JavaScript中建立一个空对象最简单的方法是Object.create(null),它不会建立Object.prototype这个委托,因此比{}更空

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
复制代码
  1. 间接引用
var a = 2
var o = {a:3,foo:foo}
var p = {a:4}
o.foo() // 3
(p.foo = o.foo)() // 2
复制代码

赋值表达式p.foo = o.foo的返回值是目标函数的引用,所以调用位置是foo()而不是p.foo()0.foo(),故这里会应用默认绑定

箭头函数看成对象的属性的值

Element.prototype.hide = () => { this.style.display = 'none' }
复制代码

会报错,查看babel解析后的代码,发现this没有绑定上:

Element.prototype.hide = function() {
    undefined.style.display = 'none'    
}
复制代码

​ 箭头函数的 this 是静态的,也就是说,只须要看箭头函数在什么函数做用域下声明的,那么这个 this 就会绑定到这个函数的上下文中。即“穿透”箭头函数。

例子里的箭头函数并无在哪一个函数里声明,因此 this 会 fallback 到全局/undefined

"穿透"到最近的词法做用域(注意对象的{}不算外层做用域),若是外层没有被函数包裹,那么就是window

例如:

let a = {
    foo() {
      console.log(this)
    },
    foo1: () => {
      console.log(this)
    },
    foo2: function foo2() {
      console.log(this)
    }
  } 
  a.foo() // a
  a.foo1() // window
  a.foo2() // a

复制代码

小结

若是要断定一个运行函数的this绑定,就须要找到这个函数的直接调用位置. 找到以后能够顺序应用下面这四条规则来判断this的绑定函数

  1. 由new调用? 绑定到新建立的对象 (new绑定)
  2. 由call或apply(或者bind)调用? 绑定到指定的对象 (显示绑定)
  3. 由上下文调用? 绑定到那个上下文对象 (隐式绑定)
  4. 默认绑定: 在严格模式下绑定到undefined,不然会绑定到全局 (默认绑定)

注意: 有些调用可能在无心中使用默认绑定规则. 若是想'更安全'地忽略this绑定,你可使用一个DMZ对象,

好比 var Ø = Object.create(null),以保护全局对象

ES6中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法做用域来决定this,具体说,箭头函数会继承外层函数调用的this绑定(不管this绑定到什么). 这其实和ES6以前的var self = this的机制同样

三. 对象

基本类型

  1. string
  2. number
  3. boolean
  4. null
  5. undefined
  6. object

注意:

null有时会被看成一种对象类型,可是这其实只是语言自己的一个bug,即对null执行typeof null时返回字符串"object". 实际上,null自己是基本类型

黑科技: 原理是这样的,不一样的对象在底层都表示为二进制,在JavaScript中二进制前三位都为0的话会被判断为object类型,null的二进制表示是全0,天然前三位也是0,因此typerof时会返回"object"

内置对象

  1. String
  2. Number
  3. Boolean
  4. Object
  5. Function
  6. Array
  7. Date
  8. RegExp
  9. Error

必要时语言会自动把字符串字面量转换成一个String对象

在对象中,属性名永远都是字符串。若是使用string之外的其余值做为属性名,它首先会被转换为一个字符串。

复制对象(待解决)

浅拷贝
深拷贝(JSON.stringify(obj)
属性描述符
1. writable

是否能够修改属性的值

2.configurable

属性是否可配置,把configurable修改为false是单向操做,没法撤销

除了没法修改,configurable:false还会禁止这个属性

3.Enumerable

属性是否会出如今对象的属性枚举中

4.不变性

全部的方法建立的都是浅不变性,它们只会影响目标对象和它的直接属性.若是目标对象引用了其余对象,其余对象的内容不受影响,仍然是可变的

4.1 对象常量

结合writable:falseconfigurable:flase就能够建立一个真正的常量属性(不可修改,不可从新定义或删除)

var object1 = {}
Object.defineProperty(object1,"FAVORITE_NUMBER",{
    value:42,
    writable:false,
    configurable:false
})
复制代码

4.2 禁止扩展

若是想禁止一个对象添加新属性而且保留已有属性,可使用Object.preventExtensions()

var myObj = { a : 2 }
Object.preventExtensions(myObj)
myObj.a = 3
myObj.a = undefined
复制代码

在非严格模式下,建立属性a会静默失败. 在严格模式下,将会抛出TypeError错误

4.3 密封

Object.seal(...)会建立一个密封的对象,这个方法实际上会在一个现有对象上调用Object.preventExtensions(...)并将全部现有的属性标记为configurable:flase

因此,密封后的对象不能添加属性,不能从新配置属性或者删除现有属性(只能修改属性的值)

4.4 冻结

Object.freeze(...)会建立一个冻结对象,这个方法实际上会在一个现有对象上调用Object.seal(...)并把全部"数据访问"属性标记为writable:fasle,这样就没法修改它们的值

这个方法是能够应用在对象上的级别最高的不可变性

5.[[Get]]

在语言规范中,obj.a在obj上其实是实现了[[Get]]操做. 对象默认的内置[[Get]]操做首先在对象上查找是否有名称相同的属性,若是找到就会返回这个属性的值

若是不管如何都没有找到名称相同的属性,那么[[Get]]操做会返回值undefined

var myObj = {
    a: 2
}
myObj.b // undefined
复制代码

这种方法和访问变量是不同的. 若是你引用了一个当前词法做用域中不存在的变量,并不会像对象属性同样返回undefined,而是会抛出一个ReferenceError异常

故,仅经过返回值,你没法判断一个属性是存在而且持有一个undefined值,仍是变量不存在,因此[[Get]]没法返回某个特定值而返回默认的undefined

6.[[Put]]

[[Put]]被触发时,实际行为分红两种:

  1. 已经存在这个属性
    1. 属性是不是访问描述符? 若是是而且存在setter就调用setter
    2. 属性的数据描述符中writable是不是false? 若是是,在非严格模式下静默失败,在严格模式下抛出TypeError异常
    3. 若是都不是,将该值设置为属性值
  1. 不存在这个属性
7.Getter函数和Setter函数

在ES5中可使用getter和setter部分改写默认操做,可是只能应用在单个属性上,没法应用在整个属性上.

getter和setter都会覆盖单个属性默认的[[Getter]]和[[Setter]]操做

8.判断属性的存在性

当咱们经过属性名访问某个值时可能返回undefined,这个值多是对象属性中存储的undefined,也有多是属性不存在返回的undefined,那么咱们怎么区分呢?

Forexample:

var obj = {
    a:2
}
('a' in obj) // true
('b' in obj) // false
obj.hasOwnProperty('a') //true
obj.hasOwnProperty('b') // fasle
复制代码

in 操做符会检查属性是否在对象及其[[Prototype]]原型链中,hasOwnProperty(...)只会检查属性是否在对象中,不会去检查[[Prototype]]

看起来in操做符能够检查容器是否有某个值,可是它实际上检查的是某个属性名是否存在.

PS:

4 in [1,2,4] // false
// 该数组包含的属性名是0 1 2 并无咱们要找的4
复制代码
9.遍历

最好只在对象上应用for...in循环中

小结

JavaScript中的对象有字面形式(var a= {...})和构造形式(var a = new Array(...))

"万物皆对象"的概念是错误的. 对象是6个或者7个(null)基础类型之一. 对象有包括function在内的子类型,不一样子类型具备不一样的行为,好比内部标签[object Array]表示是对象的子类型数组

对象就是键值对的集合. 能够经过.propName或者['propName']语法来获取属性值. 访问属性时,引擎实际上会调用内部的默认[[Get]]操做(在设置属性值时是[[Put]]),[[Get]]操做检查对象自己是否包含这个属性,若是没找到的话还会查找[[Prototype]]

属性的特性能够经过属性描述符来控制,好比writableconfigurable. 还可使用Object.preventExtensions(...),Object.seal(...)Object.freeze(...)来设置对象的不可变性级别,其中Object.freeze(...)是应用在对象上不可变性的最高级别.

属性不必定包含值-它们多是具有getter/setter的"访问描述符". 此外属性能够是可枚举或不可枚举的,这决定了它们是否会出如今for...in循环中

可使用for...of遍历数据结构(数组,对象等等)中的值,for...of会寻找内置或者定义的@@iterator对象并调用它的next()方法来遍历数据值

四. 混合对象"类"

构造函数

类实例是由一个特殊的类方法构造的,这个方法名一般和类名相同,被称为构造函数. 这个方法的任务就是初始化实例须要的全部信息

类的继承

多态(super关键字)

在传统的面向对象的语言中super还有一个功能,就是从子类的构造函数中经过super能够直接调用父类的构造函数.一般来讲这没什么问题,由于对于真正的类来讲,构造函数是属于类的.

然而,在JavaScript中刚好相反-实际上类是属于构造函数的. 因为JavaScript中父类和子类的关系只存在于二者构造函数对应的.prototype对象中,所以它们的构造函数之间并不存在直接关系,从而没法简单地实现二者的相对引用.

小结

类是一种设计模式. 许多语言提供了对于面向对象类软件设计的原生语法. JavaScript也有相似的语法,可是和其余语言中的类彻底不同

类意味着复制

传统的类实例化时,它的行为会被复制到实例中. 类被继承时,行为也会复制到子类中

多态(在继承链的不一样层次名称相同可是功能不一样的函数)看起来彷佛是从子类引用父类,可是本质上引用的实际上是复制的结果

JavaScript不会(像类那样)自动建立对象的副本, 只能复制引用,没法复制被引用的对象或者函数自己

混入模式(利用for...in遍历判断对象不存在的属性,不存在则添加)能够用来模拟类的复制行为,可是一般会产生丑陋而且脆弱的语法,好比显式伪多态(Object.methidName.call(this,....)),这会让代码更难懂而且难以维护.

显示混入实际上没法彻底模拟类的复制行为,由于对象(和函数,函数也是对象)只能复制引用,没法复制被引用的对象或者函数自己.

总地来讲,在JavaScript中模拟类是得不偿失的,虽然能解决当前的问题,可是可能会埋下更多的隐患.

五. 原型

1.[[Prototype]]

JavaScript中的对象有一个特殊的[[prototype]]内置属性,其实就是对于其余对象的引用. 几乎全部的对象在建立时[[Prototype]]属性都会被赋予一个非空的值

var anotherObj = {
    a:2
}
// 建立一个关联到 antherObj 的对象
var myObj = Object.create(anotherObj)
复制代码
1.1 Object.prototype

全部普通的[[prototype]]链最终都会指向内置的Object.prototype

1.2属性设置和屏蔽

在[第三部分对象中]提到过,给一个对象设置属性并不只仅是添加一个新属性或者修改已有的属性值

myObj.foo = 'bar'
复制代码
  • 若是myObj对象中包含名为foo的普通数据访问属性,这条赋值语句只会修改已有的属性值
  • 若是foo不是直接存在于myObj中,[[Prototype]]链就会被遍历,相似[[Get]]操做. 若是原型链找不到foo,foo就会被直接添加到myObj
  • 若是foo存在原型链上层,赋值语句myObj.foo = 'bar'的行为就会有些不一样,下面是详细的介绍
  • 若是属性名foo既出如今myObj中也出如今myObj[[Prototype]]链上层,那么就会发生屏蔽. myObj中包含的foo属性会屏蔽原型链上层的全部foo属性,由于myObj.foo老是会选择原型链中最底层的foo属性

分析若是foo不直接存在myObj中而是存在原型链上层时,myObj.foo = 'bar'会出现三种状况 >>>

  • 若是在[[Prototype]]链上层存在名为foo的普通数据访问属性而且没有被标记只读,那会直接在myObj中添加一个名为foo的新属性,它是屏蔽属性
  • 若是在[[Prototype]]链上层存在foo,可是它被标记为只读,那么没法修改已有属性或者在myObj上建立屏蔽属性. 若是运行在严格模式下,代码会抛出一个错误. 不然, 这条赋值语句会被忽略. 总之,不会发生屏蔽
  • 若是在[[Prototype]]链上层存在foo属性而且它是一个setter,那就必定会调用这个setter. foo不会被添加到myObj,也不会从新定义foo这个setter

只读属性会阻止[[Prototype]]链下层隐式建立(屏蔽)属. 这看起来有点奇怪,myObj对象会由于有一个只读foo就不能包含foo属性. 更奇怪的是,这个限制只存在于=赋值中,使用object.defineProperty(...)并不会受到影响

2."类"

2.1 类函数

经过调用new Foo()建立的每一个对象将最终被[[Prototype]]连接到这个Foo.prototype对象

function Foo() {
    ...
}
var a = new Foo()
Object.getPrototypeOf(a) === Foo.prototype // true
复制代码

在面向对象的语言中,类能够被复制屡次,就像模具制做东西同样.

可是在JavaScript中,并无相似的复制机制. 咱们不能建立一个类的多个实例,只能建立多个对象,它们的[[Prototype]]关联的是同一个对象. 可是在默认状况下并不会进行复制,所以这些对象之间并不彻底失去联系,它们是互相关联的.

关于名称

"原型继承"严重影响了你们对JavaScript机制真实原理的理解

继承意味着复制操做,JavaScript并不会复制对象属性. 相反,JavaScript会在两个对象之间建立一个关联,这样一个对象就能够经过委托访问到另外一个对象的属性和函数

2.2 "构造函数"
function Foo() {
    // ...
}
Foo.prototype.constructor === Foo // true
var a = new Foo()
a.constructor === Foo // true
复制代码

Foo.prototype默认有一个公有而且不可枚举的属性.constructor,这个属性引用的是对象关联的函数.能够看到经过"构造函数"调用new Foo(...)建立的对象也有一个.constructor属性,指向"建立这个对象的函数"

构造函数仍是调用?

new会劫持全部普通函数并构造对象的形式来调用它

function NothingSpecial() {
    console.log("Don't mind me")
}
var a = new NothingSpecial()
// Don't mind me
a // {}
复制代码

JavaScript中对于"构造函数"最准确的解释是,全部带new的函数调用

函数不是构造函数,可是当且使用new时,函数调用会变成"构造函数调用"

构造函数返回值的问题
  1. 没有返回值的状况像其余传统语言同样,返回实例化的对象
function Person(){

    this.name="monster1935";
    this.age='24';
    this.sex="male";

}
console.log(Person());  //undefined
console.log(new Person());//Person {name: "monster1935", age: "24", sex: "male"}
复制代码
  1. 若是存在返回值则检查其返回值是否为引用类型,若是为非引用类型,如(string,number,boolean,null,undefined),上述几种类型的状况与没有返回值的状况相同,实际返回实例化的对象
function Person(){

    this.name="monster1935";
    this.age='24';
    this.sex="male";

    return "monster1935";

}
console.log(Person());  //monster1935
console.log(new Person());//Person {name: "monster1935", age: "24", sex: "male"}
复制代码
  1. 若是存在返回值是引用类型,则实际返回该引用类型
function Person(){

    this.name="monster1935";
    this.age='24';
    this.sex="male";

    return {
        name:'Object',
        age:'12',
        sex:'female'
    }

}
console.log(Person());  //Object {name: "Object", age: "12", sex: "female"}
console.log(new Person());//Object {name: "Object", age: "12", sex: "female"}
复制代码

3.(原型)继承

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.myLabel = function() {
    return this.label
}
var a = new Bar("a","obj.a")
a.myName() // "a"
a.myLabel() // "obj.a"
复制代码

注意: 下面这两种方式是常见的错误作法,实际上它们都存在一些问题

Bar.prototype = Foo.prototype // 直接引用的是Foo.prototype对象,赋值语句会互相修改
// 基本知足要求,可是可能会产生一些反作用
Bar.prototype = new Foo()
复制代码

所以,要建立一个合适的对象,咱们必须使用Object.create(...)而不是使用具备反作用的Foo(...)

这样惟一的缺点就是建立一个新对象而后把旧对象抛弃掉,不能直接修改已有的默认对象

两种关联的方式
// ES6以前须要抛弃默认的Bar.prototype
Bar.prototype = Object.create(Foo.prototype)

// ES6开始能够直接修改现有的Bar.prototype
Object.setPrototypeOf(Bar.prototype,Foo.prototype)
复制代码
检查"类"关系
  • instanceOf

    a instanceOf Foo, 左边是一个普通的对象,右边是一个函数. 回答的问题是:"在a的整条[[Prototype]]链中是否有Foo.prototype指向的对象?"

  • isPrototypeOf

    Foo.isPrototypeOf(a), 回答的问题是:"在a的整条[[Prototype]]链中是否出现过Foo.prototype"

获取对象的原型链

Object.getPrototypeOf(a)

浏览器也支持一种非标准的方法访问内部的[[Prototype]]属性

a._proto_ (是可设置属性)

4.对象关联
原型链的概念

若是在对象上没有找到须要的属性或者方法引用,引擎就会继续在[[Prototype]]关联的对象上进行查找. 若是后者中也没有找到须要的引用就会继续查找它的[[Prototype]],以此类推,这一系列的连接称为"原型链"

Object.create(...)

Object.create(...)会建立一个对象并把它关联到咱们指定的对象

Object.create(null)会建立一个拥有空[[Prototype]]连接的对象,这个对象没法进行委托. 因为这个对象没有原型链,因此instanceOf操做符没法进行判断

小结

若是要访问对象中并不存在的一个属性,[[Get]]操做就会查找对象内部[[Prototype]]关联的对象. 这个关联关系实际上定义一条"原型链",在查找属性时就会对它进行遍历

全部普通对象都有内置的Object.prototype,指向原型链的顶端,若是在原型链中找不到指定的属性就会中止. toString(),valuOf()和其余一些通用的功能都存在于Object.prototype对象上,所以语言中全部的对象均可以使用他没

关联两个对象最经常使用的方法是使用new关键字进行函数调用,在调用的4个步骤中会建立一个关联其余对象的新对象

  1. 建立一个全新的对象
  2. 这个对象会被执行[[Prototype]]连接
  3. 这个新对象会绑定到函数调用的this
  4. 若是函数没有返回其余对象,那么new表达式中的函数调用会自动返回这个新对象

使用new调用函数时会把新对象的.prototype属性关联到"其余对象". 带new的函数调用一般被称为"构造函数调用",尽管它们实际上和传统面向类语言中的构造函数不同

虽然这些JavaScript机制和传统面向对象的"类初始化"和"类继承"很类似,可是JavaScript中的机制有一个核心区别,那就是不会进行复制,对象之间是经过内部的[[Prototype]]链关联的

出于各类缘由,以"继承"结尾的术语和其它面向对象的术语都没法帮助你理解JavaScript的真实机制,相比之下,"委托"是一个更合适的术语,由于对象之间的关系不是复制而是委托

六.行为委托

JavaScript中原型链这个机制的本质就行对象之间的关联关系

Js中函数之因此能够访问call(...),apply(...),bind(...)是由于函数自己是对象

1.委托理念

行为委托认为对象之间是兄弟关系,互相委托,而不是父类和子类关系.JavaScript的[[Prototype]]机制本质上就是行为委托节制. 也就是说,咱们能够选择在Js中努力实现类机制,也能够拥抱更天然的[[Prototype]]委托机制

2.类与对象

ES6的class语法糖

见下章

七. ES6中的class

传统面向类的语言中父类和子类,子类和实例之间实际上是复制操做,可是在[[Prototype]]中没有复制,相反,它们之间只有委托关联

1.class

class Widget{
    constructor(width,height) {
        this,width = width || 50
        this.height = height || 50
        this.$elem = null
    }
    render($where) {
        if(this.$elem) {
            this.$elem.css({
            	width:this.width + 'px',
            	height:this.height+'px'
            }).appendTo($where)
        }
    }
}
class Button extends Widget {
  constructor(width, height, label) {
    super(width, height)
    this.label = label || 'Default'
    this.$elem = $("<button>").text(this.label)
  }

  render($where) {
    super.render($where)
    this.$elem.click(this.onClick.bind(this))
  }

  onClick(evt) {
    console.log("Button" + this.label + 'clicked!')
  }
}
复制代码

除了语法更好看以外,ES6还解决了什么问题?

  1. 再也不引用杂乱的.prototype了
  2. Button声明直接"继承"了Widget,再也不须要经过Object.create(...)来替换.prototype对象,也不须要设置._proto_或者Object.setPrototypeOf(...)
  3. 能够经过super(...)来实现相对多态,这样任何方法均可以引用原型链上层的同名方法. 构造函数不属于类,因此没法相互引用---super()能够完美解决构造函数的 问题
  4. class字面语法不能声明属性.看起来这是一种限制,可是它会排除掉许多很差的状况,若是没有这种限制的话,原型链末端的"实例"可能会意外地获取其余地方的属性
  5. 能够经过extends很天然地扩展对象类型,甚至是内置的对象类型,好比Array或RegExp

2.class陷阱

class基本上只是现有[[Prototype]]机制(委托)的一种语法糖

也就是说class并不会像传统面向类的语言同样在声明时静态复制全部行为.若是修改或者替换了父"类"中的一个方法,那么子"类"和全部实例都会受到影响,由于它们在定义时并无进行复制,只是使用基于[[Prototype]]的实时委托

class语法没法定义类成员属性(只能定义方法)


原文地址: 传送门

Github: 欢迎Startwq93

相关文章
相关标签/搜索