《你不知道的JavaScript上卷》知识点整理与读书笔记

各位路过的的大佬。求关注、求点赞、谢谢

之前被好友推荐过《你不知道的JavaScript》这一系列的书籍。上中下三本书都不算厚,内容也是比较独立。本来是打算一个月内看完这本上卷和下卷,可总是因为自己的懒惰读了一个月才断断续续读完上卷。不得不说单是粗略的读完上卷就让我很有收获,下面是对上卷部分的读书笔记,不是什么文档和教程,仅仅供自己学习记录,欢迎各位路过的大佬能指出理解错误的地方以及意见。

第一部分 作用域和闭包

第1章 作用域是什么

1.1编译原理

首先,我也是学习JavaScript一开始,找到资料里面就说JavaScript是一门“解释型”语言。但是其实JavaScript是一门编译语言。

在传统的编译语言中,一段源代码执行前会先经过一段叫做编译的操作,而编译一共有如下3个过程。

  • 分词/词法分析(Tokenizing/Lexing)
    这个过程将把编程语言的字符串分解为有意义的代码块,这些代码块被称为词法单元(token)
var a = 2;
//就会被拆解成 var、a、=、2、; 这些都是词法单元
//而空格是否属于此法单元取决于是否有意义,
//这里的var a = 2;中的任何一个空格都没有意义
  • 解析/语法分析(Parsing)
    把众多词法单元组成的词法单元流(数组)转换成一颗树的结构,叫抽象语法树(AbstractSyntaxTree,AST)
var a = 2;
//这句语句转化为树的结构是一个叫做VariableDeclaration

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4rnlCF0N-1604651680070)(./上img/抽象语法树.png)]

  • 代码生成
    将语法树转化为可执行的代码过程。

其他的语言的编译在构建前,而JavaScript在执行前也会进行这编译3部曲

1.2理解作用域

对var a = 2;处理时候,具体的编译器是这样工作的。

//1 遇到 var a 编译器会在当前作用域中的一个集合查询是否有a这个变量,
//如果有就继续进行第2步编译动作,
//如果没有会要求当前作用域在自己的这个集合中声明一个新的a变量

//2 遇到 a = 2 编译器为这条语句生成运行时候的代码,供运行时候使用。
//这时候编译器要在生成的代码中表达出 a=2这个意思,所以在生成代码时候,
//需要一个a变量,先去找当前的作用域中的集合是否含有这个变量,
//如果没有就去上一层找。直到找到为止,否者就是报错。

在这里注意的是在步骤2中,编译器会查找作用域集合中时候会有2种不同的查找方式,一种是LHS,一种是RHS。也就是左查询与右查询。这个和我在学习C++中左值右值时候,我觉得十分相识。也就是通过最原始的“=”进行理解,左边是被填充的,右边是去填充的。
在这里插入图片描述

var a = 2;//这里编译器生成代码时候,会去当前作用域集合中找a变量,然后赋值为2。
//你可以理解为是找到这片叫做a空间,然后填充上2这个数据,
//所以这里是左查询,也就是找到被填充的a空间

console.log(a)//这里编译器生成代码时候,会去当前作用域集合中找a变量,然后打印出来。
//你可以理解为是找到这个叫做a的变量里面的值,然后使用这个值,
//所以这里是右查询,也就是找到去填充(或者理解为取出来使用)的a空间里面本身的值!!!

1.3作用域嵌套

其实书中这部分的内容讲的就是作用域链,也就是对var a = 2;处理时候,具体的编译器工作的第二步中需要一个a变量,先去找当前的作用域中的集合是否含有这个变量,如果没有就去上一层找。直到找到为止,直到最顶层没有报错为止。这就是一直往上的查找就是顺着作用域链。

var b = 3
function foo(a){
    
    
    console.log(a+b)
}
foo(2)//5
//这里的function内部是自己的函数作用域,function外面是全局作用域。
//function内部作用域没有b变量,为什么还能具体工作?
//就是因为function中没有,所以找上一级,然后上一级中就有b,就可以使用
//这种机制就是作用域嵌套,或者作用域链。
//我反正是理解为,子作用域可以使用父作用域,但是父作用域不可以反过来使用子作用域。
//当然这里的什么父、子作用域是个人为了理解,自己说的,不是什么术语。

1.5异常

前面都是顺着作用域链能查找成功的时候,但是我们也说过,如果找不到会报错。这里我们查找有2中情况。自然左右查找不到,报错是不一样的。
在这里插入图片描述

这里小小验证一下书上讲的左查询吧。

扫描二维码关注公众号,回复: 12164366 查看本文章
a = 2//这里没有var 声明。但是a=2是左查找,所以左查询热心的会在全局作用域中的集合里面创建一个空的a变量空间。
console.log(a)//右查找结果是2
"use strict"
a = 2//不好意思,这次左查询无法帮助生成一个空的a变量空间了,
//单单是这一句语句就会因为左查询失败,报ReferenceError


console.log(a)//右查找,肯定失败,这句本生也是右查找失败报ReferenceError,
//不过前一句本来就是错,压根也不会运行到这一句。

第2章 词法作用域

2.1词法阶段

首先词法作用域就是你写代码时候,书写的变量位置或者块作用域的位置就是词法作用域。而这个词法分析时候(也就前面的拆分语句var a = 2;为var、a、=、2、;),产生的作用域和写的时候一样。例如如下代码。

var a = 3;//这行语句是属于全局作用域的
function foo(a) {
    
    
    //这里面是foo函数形成的块作用域
    //写所以下面的b,bar(),以及自己的a。都是这个作用域的
    //所以解析之后任然是满足这个关系
    var b = a * 2;//这里的a是foo函数里面的,不是外面的var a = 3;里面的a
    function bar(c) {
    
    
        //这里面是bar函数形成的块作用域
        //所以自己的c是属于这里   
        console.log(a,b,c);
    }
    bar(b * 3);
}
foo(2);//2,4,12

所以词法作用域主要是体现了2点:

  • 一是一层一层往上的循着作用域找变量,这个案例中就是bar使用的a,b,c来自上一级的foo。
  • 二是,当子级的作用域有着与父级作用域同样的标识符(就是变量名、函数名)时候,子级的会覆盖(如果是知道方法重写,那么这里就理解为子类重写父类方法),这种叫做遮蔽效应。例如在foo作用域、以及子作用域中使用a时候,都是foo函数中的a,而不是foo作用域的父作用域中的var a = 3;中的a。当然,当bar中有a时候,bar中使用a时候,就会使用自己a,而覆盖foo(a)的a,与全局的var a = 3的a

2.2欺骗词法

首先记住,少使用欺骗词法,欺骗词法作用域会导致性能下降。使用eval与with关键字能实现这种。我这里就写个eval

首先eval()的用法是接受一串字符串,并把字符串的内容替换到调用的位置,有点抽象,请看代码:

function foo(str,a) {
    
    
    //这里是foo形成的词法作用域
    //先假装eval(str)不存在
    //这里只有a与str是自己的,b是外面的
    //所以调用foo("var b = 3;",1);应该是1,2的结果
    //放开注释eval(str)
    //输出为1,3
    //因为eval("var b = 3;")等同于在这行eval位置替换为
    //var b = 3;
    //因此var b = 3中的b屏蔽外面的b  

    //eval(str);
    console.log(a,b);
}
var b =2;
foo("var b = 3;",1);//1,3

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

3.1函数中的作用域

  • 函数作用域:属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用)这种设计方案能充分利用JavaScript变量可以根据需要改变值类型的“动态”特性。

3.2隐藏内部实现

  • 最小暴露原则:在软件设计中,应该最小限度的暴露最少内容,而将其他内容隐藏起来。
  • 在JavaScript中的函数作用域就可以实现隐藏代码的效果
function doSomething(a) {
    
    
    b = a + doSomethingElse(a * 2);
    console.log(b * 3);
}
function doSomethingElse(a) {
    
    
    return a - 1;
}
var b;
doSomething(2);//15

很显然这和时候我们任然能够访问到b以及doSomethingElse()函数,但是目前就这些代码而言,b以及doSomethingElse()只是为了给doSomething()使用。因此我们不应该让外部也能够访问到这两个标识符。所以应该进行隐藏

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

3.3函数作用域

尽管前面的使用函数包裹代码块达到了隐藏的意义,但是在某些时候任然是有一些缺点。一是用来包裹隐藏的函数名污染了全局变量,然后必须显示的调用函数名。但是幸好我们可以使用立即执行函数来解决。

(function foo() {
    
    
    var a = 3;
    console.log(a);    
})();
console.log(a);
//这里就可以让函数当作一个表达式,而不是函数声明
//这里就没有让foo放出去污染全局,因为他不是声明
//这里最后紧紧跟了一个(),表示立即调用,因此不用显示使用标识符调用
//当然把最后的()放进去的效果也是一样的,仅仅是书写风格不同。如下
(function foo() {
    
    
    var a = 3;
    console.log(a);    
}());
console.log(a);
  • 那么时候是函数表达式呢?

Good question

区分:书上的办法很简单,就是查看function关键字出现在声明中的位置(不仅仅是一行代码,而是整个声明中的位置)。如果function是声明中的第一个词,那么就是一个函数声明,否者就是一个函数表达式。

上面的代码中(function foo(){…})()就不是function开头,这个时候就是作为表达式,而foo标识符就是被封锁在foo(){…}中…的部分中,所以就没有污染全局变量

  • 匿名和具名
    匿名函数就是没有名字的函数
setTimeout(function(){
    
    
    console.log("I waited 1 second");
},1000);

这部分的片段代码中seTimeout中function()就是没有名字。所以就是匿名函数。

  • 注意:之后函数表达式才能拥有这种匿名函数,函数表达式没有函数名。请看前面的代码,这里可是函数表达式哈。

3.4块作用域

现在我们学会了词法作用域、函数作用域以及一些小的知识点。现在学习新的作用域、块作用域。

前面说过,使用函数作用域能够隐藏一段代码,而隐藏也就是具有隔离意思。把一段代码隔离到一个函数中,让其外界无法触碰。但是并不是使用外包函数就能解决。

//这行代码只是为了验证()中的var i,以及{}中的a,最后在外部均可以访问到这两个标识符

for(var i = 0; i < 10; i++){
    
    
    console.log("for里面调用i",i);
    var a = 5;
}
console.log("for外面调用for里面的a",a); //for外面调用for里面的a 5  
console.log("for外面for里面的i",i);//for外面for里面的i 10

下面是使用函数包裹

(function(){
    
    
for(var i = 0; i < 10; i++){
    
    
    console.log("for里面调用i",i);
    var a = 5;
}
}())
console.log("for外面调用for里面的a",a);//VM22:7 Uncaught ReferenceError: a is not defined  
console.log("for外面调用i",i);//Uncaught ReferenceError: i is not defined

是完成了封锁,可是这样情况并不简便,所以开发者为JavaScript提供了块作用域。我觉得其实就和完成了函数作用域一样的功能,封锁代码块在其{}中。

  • let

let声明的变量,是会和当前的作用域进行绑定。

for(let i = 0; i < 10; i++){
    
    
    console.log("for里面调用i",i);
    var a = 5;
}
console.log("for外面for里面的i",i);//Uncaught ReferenceError: i is not defined

恭喜,i已经绑定在for体内了,是不是方便很多了呢。

注意let i,不仅仅是把i绑定到了for循环的{}中,准确的说是绑定到了每一个迭代中,这点十分十分重要。后面闭包时候会再次提到这个事。同时let声明的标识符不会进行提升,这一点后面的提升相关部分会进行说明

  • const
    const声明的变量,是会和当前的作用域进行绑定,并且这个变量的值是固定的,不能再更改

  • try/cath
    没想到吧,居然每一个cath分支都是一个块作用域,不相信的话,可以动手试一试

第4章 提升

首先得出结论

  • 只有声明本身会被提升,而赋值或者其他运行的逻辑会留在原地。如果是提升改变的代码执行的顺序,会造成严重的破坏
  • 声明包括变量声明以及函数声明
  • 每个作用域都会进行提升
  • 函数和变量提升时候,函数会优先。因为函数是一等公民
  • let以及const声明的标识符不会提升
foo();//1
var foo;
function foo() {
    
    
    console.log(1);
}
foo = function () {
    
    
    console.log(2);
}

上面代码会被引擎理解为如下形式:

function foo() {
    
    
    console.log(1);
}
foo();//1

foo = function () {
    
    
    console.log(2);
}
foo();//2

所以函数是优先提升的。

第5章 作用域和闭包

5.1 总结

我喜欢开篇,直接就总结完。

闭包产生的2种情况

  • 当函数作为另一个函数的参数
  • 函数作为返回值返回
function foo() {
    
    
    var a = 2;
    function bar() {
    
    
        console.log(a);
    }
    return bar;
}
var baz = foo();
baz();//2 朋友,这就是闭包效果

5.2 循环和闭包

要说明闭包,for循环是一个常见的例子

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

这段代码再运行时候会每秒一次的频率输出5次6.

延迟函数的回调会在全部循环迭代结束的之后时候进行调用(请查询宏任务、微任务相关知识点),而不是每次迭代时候调用。所以最后调用i,但是i是公共的,并且值为最后一个循环决定的6。所以结果是5次6

那怎么给每个迭代的版本获取一个实时的i,满足哪怕是最后循环迭代完再调用定时函数,但是每个定时函数都是调用自己版本的,而不是调用最后的公用6呢?

那就是每次循环迭代时候,我们给每一个迭代都绑定一个i。如下所示,我们使用let让每一个i都再内部迭代进行绑定。

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

5.3 模块

在js中的模块也是和闭包息息相关。

模块:

  • 必须要有外部的封闭函数,该函数必须要被调用一次
  • 封闭的函数至少要返回一个内部函数
  • 使用立即执行函数配合有奇效
var foo = (function(){
    
    
    var something = "cool";
    var another = [1,2,3];
    function doSomething() {
    
    
        console.log(something);
    }
    function doAnother() {
    
    
        console.log(another.join("!"));
    }
    return {
    
    
        doSomething,
        doAnother
    }
})();
foo.doSomething();//cool
foo.doAnother();//1!2!3

第二部分 this和对象原型

第1章 关于this

1.1为什么要使用this

首先记住,this提供了一种更优雅的方式隐式的传递一个对象引用,从而使API设计更加的简洁,并且更加易于复用

1.2对this的一些误解

  • 误解一、指向自身
    按照this这个单词的语意,我们总是会把他认为是指向自身,事实上有些时候确实如此,但是并不是总指向自身。分析下面的模式。
function foo(num) {
    
    
    //记录count被调用的次数
    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.count会在全局创建一个变量,值为NaN。至于为什么后面第2章在解释,这里只是为了说明this真的不是任何时候指向自己。下面是单独打印的结果。

(function foo(num) {
    
    
    //记录count被调用的次数
    this.count++;
    console.log(this);//window,也就是全局
    console.log(this.count);//NaN
}())
  • 误解二、指向他的作用域
    首先这里this有时候指向作用域,有时候又不是,但是明确的一点就是任何时候this都是不会指向他的词法作用域。因为词法作用域是属于引擎的,无法通过js代码进行访问。

首先明确一下前面的案例中foo()中的this是指向window,这里我们在前面的基础上改一改

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

明明this是window,也就是全局,然后bar也在全局,this.bar()没问题,但是因为bar中有this,按照词法作用域,bar在foo中,this.a右查询,就会查询bar的作用域中的a,但是没有就向上面找,上面foo中有a,所以调用。可是事实呢,结果是找不到,说明this.a进行右查询之后压根没有往上,或者往上后查找失败。(学了后面知道,是没有往上,因为bar中的this不是指向bar作用域,而是全局,所以最顶层没有,也不往上,直接undefined。至于是为什么bar中的this以及foo中的this均为window,请看后面的章节)

第2章 this全面解析

2.1调用位置

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

function foo() {
    
    
    function bar() {
    
    
   }
   bar();//在foo中声明,所以bar的调用位置在foo中
}
foo();//在全局中声明,所以foo内的调用位置在window上

2.2绑定规则

  • (1)默认绑定
    this绑定在window上。这个时候满足:查看函数调用位置时候,函数是光秃秃的直接调用,比如前面2.1中的bar,尽管是在foo中调用,可以打印看看bar中this是不是指向window

  • (2)隐式绑定
    this绑定到某个对象上。这个时候满足,这个对象的一个属性是一个函数,并且调用这个函数属性时候,是通过对象.函数名()或者对象函数名调用。否者会出现隐式丢失。

var a = 3;//我是全局的3
function foo(){
    
    
    console.log(this.a);
}
var obj = {
    
    
    a:2,//我是对象的2
    foo:foo
};

obj.foo();//2,this绑定的是obj
obj["foo"]();//2,this绑定的是obj

var bar = obj.foo;//注意,这里可不是调用,后面没有(),这里只是拿出来
bar();//结果是3,明明bar就是foo函数,
//但是注意,这里并不是我们说的调用方法的两种之一
//所以呀,绑定丢失。而这里,
//bar的调用位置是光秃秃的直接调用,
//所以这里this绑定到window上咯 
  • (3)显式绑定
    this绑定到某个对象上,但是其变种不会产生隐式丢失。这种方法很粗暴,直接使用call(),或者apply()。这两个函数,直接修改this的绑定对象。
function foo(){
    
    
    console.log(this.a);
}
var obj = {
    
    
    a:2,
    foo:foo
};
foo.call(obj);//2。也就是把foo函数的this,绑定到obj上

可是在进行如下调用时候,也会丢失

var a = 3;//我是全局的3
function foo(){
    
    
    console.log(this.a);
}
var obj = {
    
    
    a:2,
    foo:foo
};
foo.call(obj);

//下面是调用
var bar = obj.foo;
bar()//是3,哈哈哈

显示绑定的变种的核心思想都是外面包裹一个函数,每次调用这个包裹函数就好,哪怕是在调用位置光秃秃调用,都不会绑定到全局的window,因为调用包裹函数,每次自动调用一次apply或者call。下面是变种的写法。

//变种一,直接包裹一个函数
var a = 3;//我是全局的3
var obj = {
    
    
    a:2,//我是对象的2
    foo:foo
};
function foo(){
    
    
    console.log(this.a);
}
function bar() {
    
    
    return foo.call(obj);
}

bar();//2  所以每次这样就好,哈哈哈
  • (4)new绑定
    首先JavaScript中new和其他语言中的new是完全不同。
    JS中的new:使用new来调用构造函数,但JS中构造函数不属于某个类,也不会实例化某个类。(关于更多的解释,在第6章)

new的机制为:
创建一个全新的对象
这个新对象执行[原型]链接
这个新对象绑定到函数调用的this中
如果函数没有返回其他对象,new自动返回这个新对象

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

2.3优先级

不举例了,这里直接总结结论。

  • new最大
  • 显示
  • 隐式
  • 默认

2.4箭头函数

箭头函数是ES6中新东西,不遵循上面的4条绑定规则。而是根据外层作用域决定的。

var a = 3;
var obj = {
    
    
    a:2
};
function foo(){
    
    
   ((a)=>{
    
    
        //this继承foo
        console.log(this.a);
    })()
}
foo()//3 这里光秃秃调用,foo中this是window,所以回调中是window的a
var a = 3;
var obj = {
    
    
    a:2
};
function foo(){
    
    
   ((a)=>{
    
    
        //this继承foo
        console.log(this.a);
    })()
}
foo.call(obj)//2 这里显示调用,foo中this是obj,所以回调中是obj的a

第3章 对象

3.1对象定义的语法

有两种方式,一种是通过声明形式,一种是通过构造形式

//声明
var obj = {
    
    
    key:value
}

//构造
var obj = new Object();

3.2类型

对象有6种基本类型

  • string
  • number
  • boolean
  • null
  • undefined
  • object
    当然这些基本类型本身不是对象,只是说对象是根据基本类型划分有这么几个类型。不明白的可以继续看下去。

内置对象
JS中有许多特殊的对象子类型,被成为内置对象。下面的一些内置对象的名字和基本类型相似,但是不是同一个东西。下面的是实实在在的一个对象,不像前面的基本类型只是判别标准

  • String
  • Number
  • Boolean
  • Object
  • Function
  • Array
  • Date
  • RegExp
  • Error
    这些内置的对象好比java语言中的包装类,但是你先明白JavaScript中的类都是函数,是通过函数表达类的表象(这是后话,后面的第四章会有详细的类的讲解)

3.3对象中的内容

首先要强调一点,我们说的内容是,似乎暗示说这些值在对象内部,但是其实不是。如果是学过c语言或者c++的指针,或者明白说java中的引用。理解这一点是很容易的。在js中的对象的内容,对象中对的属性并不保存某些值,而是通过引用的方式,存放的是一些真正值的地址。

书中的3.3.2中,再次阐述,一个函数与一个对象的关系。我通读完后,觉得其实就是任然说明和前面this隐式绑定一个对象的道理一样。一个函数无论如何也不要理解为属于一个对象。我们应该理解为这个对象拥有这个函数,或者说这个对象目前是这个函数的落脚点。毕竟,在对象中,如果某个属性是函数,那么这个属性保存的值其实是这个函数的引用而已。

特别注意数组也是js中的子对象Array。然后尽管是数组按照组织下标的方式存储数据,但是你也可以为数组添加key:value的形式的内容。还记得吗,数组不是有一个length属性吗,这个就是最好的例子。

var array = ["foo","22"];
array.bar = "bar";
console.log(array.bar)//bar

现在关于引用,探讨对对象的内容的拷贝。因为引用的存在,所以出现了深浅拷贝。浅拷贝是拷贝引用,深拷贝而是彻底的进行复制一份数据。而在修改,数据时候,又牵扯到属性描述符以及setter与getter。下面的这两篇博客中部分内容对此进行了总结。

对象

getter与setter

3.4对象中的遍历

for…in循环可以用来遍历对象的可枚举属性列表(包括[prototype]链),但是如何遍历属性的值???对于数组来说:

var array = [1,2,3];
for (let i = 0; i < array.length; i++) {
    
    
    console.log(array[i]);
}

这可不是遍历数组的值,这是遍历数组的下标,而不是值!!!
通过这个说明了通过for…in遍历值是不行。但是对于数组而言有一些方法可以进行值的遍历,(针对数组的遍历的方法,由于不是这一节的重点,不总结进来)

但是对与其他的对象呢?ES6中就专门增加了for…of结构遍历值(当然数组也可使用这个哈)

var array = [1,2,3];
for(var v of array){
    
    
    console.log(v);
}
//1
//2
//3

好神奇,成功了。可是这种for…of的核心是怎么工作的呢?

for…of会首先向被迭代对象请求一个迭代器对象,然后通过调用迭代器对象的next()方法来遍历所有的返回值。

某种数据结构中必须要有@@iterator,才可以给for…of一个迭代器对象

数组中有内置的@@iterator,所以可以给for…of一个迭代器对象

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

it.next();//{value: 1, done: false}
it.next();//{value: 2, done: false}
it.next();//{value: 3, done: false}
it.next();//{value: undefined, done: true}
  • 我们使用Symbol.iterator获取@@iterator内部属性,关于点击这里Symbol,请记住引用类似的类似iterator特殊属性时候,要使用符号名array[Symbol.iterator],而不是单独一个array[iterator]
  • array[Symbol.iterator]也就是@@iterator本身不是迭代器,而是一个返回迭代器的函数,所以上面的的代码中,通过后面()调用,返回一个迭代器,然后赋值给it

所以最后,让我们按照上面的2条,我们自己为一个遍历的对象实现一个迭代器

var obj = {
    
    
    a:2,
    b:3
}
//下面代码的意义在于给obj定义一个Symbol.iterator属性,这个属性是一个函数,用来返回迭代器
Object.defineProperty(obj,Symbol.iterator,{
    
    
    enumerable:false,
    writable:false,
    configurable:true,
    //看到了吗,这个属性值是一个函数
    value:function () {
    
    
        var o = this;//也就是这个对象本身,想一想为什么this是指向这个对象本身,我保证前面的this里面讲的知识,绝对可以分析出来这里的this指向哪里
        var idx = 0;
        var ks = Object.keys(o);//返回这个对象的键值列表
        return{
    
    
            next:function () {
    
    
                return{
    
    
                    value:o[ks[idx++]],
                    done:(idx>ks.length)
                }
            }
        }
    }
})

//使用for...of遍历
for(var v of obj){
    
    
    console.log(v);
}

  • 这一章需要有一定的类的基础,不然读起来真的云里雾里。如果没有请先查阅其他资料补习。

4.1类理论

  • 类/继承描述了一种代码的组织结构形式。
  • 多态其实就是说父类的通用行为可以被子类用更加特殊的行为重写。(甚至相对多态允许我们从重写行为中引用基础行为)

面向对象编程强调的是数据和操作数据的行为本质上是相互关联的,因此好的设计就是把数据以及它相关的相关行为打包(封装)起来,有时候这种情况被叫做数据结构。

  • 类只是一种模式,而不是必须的!当然java,c#这些语言,没得选,只能使用类模式。
  • JavaScript中并不是必须采用的类模式。现在JavaScript中的“类”,也只是近似类。记住一点,js的“类”和他们都不一样,这也是加双引号的原因。

4.2类的机制

书中表达的观点是,类和实例对象之间的关系看作之间关系而不是间接关系更好。因为所谓的类的关系,都是复制而已。具体怎么复制,以及细节,看下面。

构造函数就是复制的关键点。他的目的就是完成复制的关键。术语叫做初始化对象。

构造函数的特点

  • 使用new来调用构造函数
  • 函数名与类名相同
  • new的过程,请看前面讲解this时候笔记

4.3类的继承

下面的书中的例子,是我觉得这个章节中最最最精彩的部分
下面的伪代码例子说明了2点:

  • 1、在js中的继承就是复制!!!
  • 2、js中的多态!!!
  • 3、js中的相对多态!!!
//下面几个类均不含构造方法

class Vehicle{
    
     //交通工具类
    engines = 1 //交通工具的属性

    ignition(){
    
    //交通工具的方法
        output("Turning on my engine , Vehicle!")
    }

    drive(){
    
    //交通工具的方法
        ignition();
        output("Steering and moving forward")
    }
}

class Car inherits Vehicle{
    
     //一个继承交通工具类的汽车类
    wheels = 4//汽车的属性

    drive(){
    
    //这里就是子类重写父类的方法
        inherited:drive();//这里就是子类引用父类基础行为,这就是相对多态
        output("Steering and moving forward")//这里就是子类更加特殊的行为,这就是多态
    }
}

class SpeedBoat inherits Vehicle{
    
     //一个继承交通工具类的快艇类
    engines = 2//快艇的属性

    ignition(){
    
    //快艇的方法 这里就是子类重写父类的方法
        output("Turning on my engine ,SpeedBoat!")//这里就是子类更加特殊的行为,这就是多态
    }

    drive(){
    
    //快艇的方法 子类重写父类的方法
        inherited:drive();//这里就是子类引用父类基础行为,这就是相对多态
        output("SpeedBoat through the water with ease")//这里就是子类更加特殊的行为,这就是多态
    }
}

好啦,上面的2、3点都写在注释中了,可是“在js中的继承就是复制!!!”,我们还没有得到解释。

仔细看一个有趣的点:

  • (1)SpeedBoat中有一个drive方法对吧
  • (2)这个是引用的方法对吧
  • (3)所以SpeedBoat中的drive方法来自父类,自然里面的代码是这样
     drive(){
          
          //交通工具的方法
        ignition();
        output("Steering and moving forward")
    }
    
  • (4)这个时候drive里面继续调用ignition(),确实没毛病
  • (5)回顾一下前3点:这个SpeedBoat中的drive来自Vehicle,这个drive调用ignition
  • (5)问题来了,SpeedBoat 与 Vehicle均有ignition,调用那个的呢?
  • (6)答案是SpeedBoat
  • (7)这说明了什么?

在js中的继承就是复制!!! 调用的是SpeedBoat中的,而不是Vehicle,说明了他们两个类没有所谓的关系,SpeedBoat就是Vehicle的一个复刻品,只不过后面这个复刻品有了一些自己的新东西(多态)。如果不是复制,为啥从Vehicle继承的drive,不去找Vehicle中调用ignition,而是去SpeedBoat中?直接从SpeedBoat使用ignition,说明SpeedBoat已经复制了Vehicle的一切,就使用自己,而不是去父类中找。

抱歉,鉴于我的表达能力,上面的话说的有些绕,不明白请看原书第一版的p131-p133

4.4混入

上面解释了js中的继承就是复制,这里混入就是讲解几种种不同复制方式

  • 显示混入
  • 隐式混入
    这里就不总结前两个了,问题比较多,感觉不怎么使用到。这里提前补充一个使用原型进行混入,看不懂可以跳过这段
function Animal(name) {
    
    
    this.name = name
}
Animal.prototype.showName = function () {
    
    
    console.log("我的名字是" + this.name)
}

function Dog(name,color) {
    
    
    //强制吧animal中的this,绑定当new出来的对象,
    //不知道为啥this指向new出来的对象的同学
    //请看前面this部分,以及new的几个过程
    Animal.call(this,name)//只能继承属性
    this.color = color
}
Dog.prototype = new Animal()
Dog.prototype.constructor = Dog

第5章 原型

第6章 行为委托

未完待续,抱歉一直拖着,这个周六整理完最后l两章。都看到这里了,喜欢就点个关注加收藏吧,卑微求个一键三连。谢谢各位大佬们了。

猜你喜欢

转载自blog.csdn.net/qq_45549336/article/details/109534465