JavaScript高级之理解原型原型链及异步编程——前端之JavaScript高级之五【Day50-Day56】

261 阅读17分钟

挑战坚持学习1024天——前端之JavaScript高级 本文章介绍原型原型链及异步编程,结合自己的理解以及实践。以下是一些近期的感悟。 无为之用方位大用,做事做人一定要遵循“道”,人无法胜天,摸索道顺应道方是比较快的路。道是看不见摸不着的,形而上学为之道,形而下学为之器。

js基础部分可到我文章专栏去看 ---点击这里

Day50【2022年9月12日】

休息

Day51【2022年9月13日】

学习重点: 原型原型链初步理解

1.对原型原型链的理解

对于使用过基于类的语言 (如 Java 或 C++) 的开发者们来说,JavaScript 实在是有些令人困惑 —— JavaScript 是动态的,本身不提供一个 class 的实现。即便是在 ES2015/ES6 中引入了 class 关键字,但那也只是语法糖,JavaScript 仍然是基于原型的。

当谈到继承时,JavaScript 只有一种结构:对象。每个实例对象(object)都有一个私有属性(称之为 proto )指向它的构造函数的原型对象(prototype)。该原型对象也有一个自己的原型对象(proto),层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。

几乎所有 JavaScript 中的对象都是位于原型链顶端的 Object 的实例。

尽管这种原型继承通常被认为是 JavaScript 的弱点之一,但是原型继承模型本身实际上比经典模型更强大。例如,在原型模型的基础上构建经典模型相当简单。

1.1 理解原型

原型在 JavaScript 中,每个构造函数都拥有一个 prototype 属性,它指向构造函数的原型对象,这个原型对象中有一个 construtor 属性指回构造函数;每个实例都有一个__proto__属性,当我们使用构造函数去创建实例时,实例的__proto__属性就会指向构造函数的原型对象。

以下为补充: 无论何时,只要创建一个函数,就会按照特定的规则为这个函数创建一个 prototype 属性(指向 原型对象)。默认情况下,所有原型对象自动获得一个名为 constructor 的属性,指回与之关联的构 造函数。对前面的例子而言,Person.prototype.constructor 指向 Person。然后,因构造函数而 异,可能会给原型对象添加其他属性和方法。

在自定义构造函数时,原型对象默认只会获得 constructor 属性,其他的所有方法都继承自 Object。每次调用构造函数创建一个新实例,这个实例的内部[[Prototype]]指针就会被赋值为构 造函数的原型对象。脚本中没有访问这个[[Prototype]]特性的标准方式,但 Firefox、Safari 和 Chrome会在每个对象上暴露__proto__属性,通过这个属性可以访问对象的原型。在其他实现中,这个特性完全被隐藏了。关键在于理解这一点:实例与构造函数原型之间有直接的联系,但实例与构造函数之间没有。

proto: 每个对象都有这个属性包括函数对象以及普通对象,它也是一个对象包括两个属性,constructor和__proto__。也叫隐藏式原型。 Firefox、Safari 和 Chrome会在每个对象上暴露__proto__属性,通过这个属性可以访问对象的原型。

prototype: 原型对象有一个默认的constructor属性。函数对象才有这个属性,它是构造函数的原型对象。显示原型

construtor : 这是原型对象上的一个指向构造函数的属性。

Object、Function都是js内置的函数, 类似的还有我们常用到的Array、RegExp、Date、Boolean、Number、String

实例

//原型 原型链
//创建Dog构造函数
function Dog(name,age){
    this.name = name;
    this.age = age;
}

//创建Dog的原型对象
// Dog.prototype = {
//     constructor:Dog,
//     eat:function(){
//         console.log("吃饭");
//     }
// }
Dog.prototype.eat = function(){
    console.log("吃饭");
}

//创建Dog的实例对象
var dog1 = new Dog("小黑",3);

//测试
console.log(dog1);

console.log(dog1.__proto__ === Dog.prototype); //true
console.log(dog1.constructor === Dog); //true
console.log(Dog.prototype.constructor === Dog); //true
console.log(dog1.eat === Dog.prototype.eat); //true
console.log(Dog.prototype.constructor === dog1.constructor)//true
console.log(dog1.eat.constructor === Dog.prototype.eat.constructor); //true
console.log(dog1.eat()); //吃饭


image.png

1.2 理解原型链

ECMA-262 把原型链定义为 ECMAScript 的主要继承方式。其基本思想就是通过原型继承多个引用 类型的属性和方法。重温一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型有 一个属性指回构造函数,而实例有一个内部指针指向原型。如果原型是另一个类型的实例呢?那就意味 着这个原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函 数。这样就在实例和原型之间构造了一条原型链。这就是原型链的基本构想。

//原型 原型链
//创建Dog构造函数
function Dog(name,age){
    this.name = name;
    this.age = age;
}

//创建Dog的原型对象
// Dog.prototype = {
//     constructor:Dog,
//     eat:function(){
//         console.log("吃饭");
//     }
// }
Dog.prototype.eat = function(){
    console.log("吃饭");
}

//创建Dog的实例对象
var dog1 = new Dog("小黑",3);

//测试
console.log(dog1);

console.log(dog1.__proto__ === Dog.prototype); //true
console.log(dog1.constructor === Dog); //true
console.log(Dog.prototype.constructor === Dog); //true
console.log(dog1.eat === Dog.prototype.eat); //true
console.log(dog1.eat.constructor === Dog.prototype.eat.constructor); //true
console.log(dog1.eat()); //吃饭

dog1.eat() // 吃饭
// 输出"[object Object]"
dog1.toString()

明明没有在 dog1 实例里手动定义 eat 方法和 toString 方法,它们还是被成功地调用了。这是因为当我试图访问一个 JavaScript 实例的属性 / 方法时,它首先搜索这个实例本身;当发现实例没有定义对应的属性 / 方法时,它会转而去搜索实例的原型对象;如果原型对象中也搜索不到,它就去搜索原型对象的原型对象,这个搜索的轨迹,就叫做原型链。

以我们的 eat1 方法和 toString 方法的调用过程为例,它的搜索过程就是这样子的:

image.png

楼上这些彼此相连的 prototype,就构成了所谓的 “原型链”。

注: 几乎所有 JavaScript 中的对象都是位于原型链顶端的 Object 的实例,除了 Object.prototype(当然,如果我们手动用 Object.create(null) 创建一个没有任何原型的对象,那它也不是 Object 的实例)。

2.今日精进

用勇气改变可以改变的事情,用胸怀接受不能改变的事情,用智慧分辨两者的不同。

Day52【2022年9月14日】

学习重点: 原型原型链深入理解及运用 补充一下什么是构造函数,构造函数的提出实质上是为了对代码进行封装,减少重复代码的使用,具体可看这篇文章点击这里

1.原型链图解

这是经典原型的图

image.png 用自己的方式画一下

eb1b6b333d2f9836bbac126b23bbf4a.png 这是前提条件

function Foo(){
    
}

let f1 = new Foo();
let o1 = new Object();

以此可以推演出原型之间的关系:(待补充)

function Foo() {

}

let f1 = new Foo();
let o1 = new Object();
// f1 Foo Foo.prototype 之间的三角关系
console.log(f1 instanceof Foo); // true 原型链上可以找到 f1的原型
console.log(typeof Foo); // function
console.log(typeof Foo.prototype); // object
console.log(f1.__proto__ === Foo.prototype); // true
console.log(f1.constructor = Foo); // true f1的构造函数是Foo
console.log(f1.__proto__.constructor === Foo); // true
console.log(Foo === Foo.prototype.constructor); // true
console.log(Foo.prototype.__proto__ === f1.__proto__.__proto__); // true


// f1 Foo Foo.prototype object 之间关系 原型链的终端的是null
console.log(f1 instanceof Object); // true
console.log(Foo.prototype instanceof Object); // true
console.log(f1.__proto__.__proto__ === Object.prototype); // true
console.log(f1.__proto__.__proto__.__proto__ === null); // true
console.log(Foo.prototype.__proto__ === Object.prototype); // true
console.log(Foo.prototype.__proto__.constructor === Object); // true
console.log(Object.prototype.constructor === Object); // true
console.log(Foo.prototype.__proto__.__proto__ === null); // true
console.log(Object.prototype.__proto__ === null); // true

// f1 Foo Foo.prototype Function 之间关系
// Foo 构造函数由Function构造
console.log(Foo.__proto__ === Function.prototype); // true
console.log(Foo.prototype.__proto__ === Object.prototype); // true
console.log(Function.prototype.__proto__ === Object.prototype); // true
//因为前面两者推出这个关系
console.log(Foo.prototype.__proto__ === Function.prototype.__proto__); // true
//又 Foo.prototype === f1.__proto__ 
console.log(f1.__proto__.__proto__ === Function.prototype.__proto__); // true
console.log(f1.__proto__.constructor.__proto__ === Function.prototype); // true

// o1 Object Object.prototype 之间关系
//从o1出发
console.log(o1 instanceof Object); // true
console.log(o1.constructor === Object); //true  o1 的构造函数是Object
console.log (o1.constructor === o1.__proto__.constructor); // true
console.log(o1.__proto__ === Object.prototype); // true
console.log(o1.__proto__.constructor === Object); // true
console.log(o1.__proto__.__proto__ === null); // true
//从构造器的角度说Object是由Function构造的
console.log(o1.__proto__.constructor.__proto__ === Function.prototype); // true 
console.log(o1.__proto__.constructor.__proto__.__proto__ === Object.prototype); // true
console.log(o1.__proto__.constructor.__proto__.constructor === Function); // true
console.log(o1.__proto__.constructor.__proto__.__proto__.__proto__ === null); // true
//从Object出发 从构造器的角度说Object是由Function构造的
console.log(Object.__proto__ === Function.prototype); // true
console.log(Object.prototype.__proto__ === null); // true
console.log(Object.__proto__.constructor === Function); // true
console.log(Object.prototype.constructor === Object); // true
console.log(Object.prototype.constructor.__proto__ === Function.prototype); // true
console.log(Object.prototype.constructor.__proto__.constructor === Function); // true
console.log(Object.prototype.constructor.__proto__.__proto__ === Object.prototype); // true
console.log(Object.prototype.constructor.__proto__.__proto__.__proto__ === null); // true

// Function Function.prototype 之间关系
// Function是由Function构造的 Function = new Function(); 所以其自身__proto__指向Function.prototype
console.log(Function.__proto__ === Function.prototype); // true
console.log(Function === Function.prototype.constructor) // true
console.log(Function.prototype.constructor.__proto__ === Function.prototype); // true
console.log(Function.prototype.constructor.prototype === Function.prototype); // true
console.log(Function.prototype.constructor.__proto__.constructor === Function); // true
console.log(Function.prototype.__proto__ === Object.prototype); // true
console.log(Function.__proto__.__proto__ === Object.prototype); // true
console.log(Function.prototype.__proto__.constructor === Object); // true
console.log(Function.prototype.__proto__.__proto__ === null); // true
console.log(Function.__proto__.__proto__.__proto__ === null); // true
console.log(Function.prototype.constructor.__proto__.__proto__ === Object.prototype); // true


console.log(Function instanceof Object) // true
console.log(Object instanceof Function) // true

Function是最顶层的构造器,Object是最顶层的对象;从原型链讲Function继承了Object,从构造器讲Function构造了Object。

2.真题

2.1原型加构造函数基础

var A = function() {}; // 创建一个构造函数A
A.prototype.n = 1; // 给A的原型添加一个属性n
var b = new A(); // new一个A的实例b 这里执行了new A()的时候,会执行A.prototype = {n:1},所以b.__proto__ = A.prototype
// new 操作符做了这些事情:
// 1.创建一个新对象
// 2.执行构造函数中的代码(为这个新对象添加属性) 将新对象的 _ proto_ 这个属性指向对应构造函数的 prototype 属性,把实例和原型对象关联起来
// 3.将构造函数的作用域赋给新对象(因此this就指向了这个新对象)
// 4.执行函数体内的逻辑,最后即便你没有手动 return,构造函数也会帮你把创建的这个新对象 return 出来
// 第二步执行玩的时候实例对象的原型就把构造函数的 prototype 的引用给存下来了
//执行完以上操作过后 再对A的原型添加一个属性n2 相当于是一个赋值操作,不会再对b有影响
A.prototype = {
  n: 2,
  m: 3
}
var c = new A(); // new一个A的实例c

console.log(b.n); // 1
console.log(b.m); // undefined

console.log(c.n); // 2
console.log(c.m);  // 3

console.log(b.__proto__ ); //{n: 1, constructor: ƒ}
console.log(c.__proto__ ); //{n: 2, m: 3, constructor: ƒ}
console.log(A.prototype); //{n: 2, m: 3, constructor: ƒ}

实际上以上考察的是原型链知识以及new操作符和构造函数

2.2自有属性与原型继承属性

function A() { // 创建一个构造函数A
    this.name = 'a'
    this.color = ['green', 'yellow']
 }
 function B() { // 创建一个构造函数B
 }
 // B.prototype = new A(); B的原型指向A的实例 加入中间人b
 B.prototype = new A() // B的原型指向A的实例
 var b1 = new B() // new一个B的实例b1
 var b2 = new B() // new一个B的实例b2
 
 //赋值操作
 b1.name = 'change' // 给b1的name属性赋值
 //没有改变对象的引用,只是改变了对象的值 它走的就是 原型链 查询 + 修改 的流程,而非原地创建新属性的流程。
 b1.color.push('black') // 给b1的color属性添加一个元素 'black' 

 //赋值操作,不会再对b2有影响
 //b1.color = ['red', 'blue'] // 给b1的color属性赋值

console.log(b2.name) // 'a'
console.log(b2.color) // ["green", "yellow", "black"]

原型图如下:

image.png 涉及到的读写操作

2.3读操作与写操作的区别

b1 和 b2 之间的一个区别就是 b1 有自有的 name 属性。

b1.name = 'change'

在查找 b1 的 name 属性时,沿着原型链去找,然后定位并修改原型链上的 name 实际上,这个 “逆流而上” 的变量定位过程,当且仅当我们在进行 “读” 操作时会发生。

楼上这行代码,是一个赋值动作,是一个 “写” 操作。在写属性的过程中,如果发现 name 这个属性在 b1 上还没有,那么就会原地为 b1 创建这个新属性,而不会去打扰原型链了。

那么 color 这个属性,看上去也像是一个 “写” 操作,为什么它没有给 b2 新增属性、而是去修改了原型链上的 color 呢?首先,这样的写法:

 b1.color.push('black')

包括这样的写法(修改对象的属性值):

b1.color.attribute = 'xxx'

它实际上并没有改变对象的引用,而仅仅是在原有对象的基础上修改了它的内容而已。像这种不触发引用指向改变的操作,它走的就是 原型链 查询 + 修改 的流程,而非原地创建新属性的流程。 如何把它变成写操作呢?直接赋值:

b1.color = ['newColor']

这样一来,color 就会变成 b1 的一个自有属性了。 因为 [‘newColor’] 是一个全新的数组,它对应着一个全新的引用。对 js 来说,这才是真正地在向 b1 “写入” 一个新的属性。

特别要注意这类题目

2.4构造函数综合考察

function A() {} // 创建一个构造函数A
function B(a) { // 创建一个构造函数B
    this.a = a; // 给B的实例添加一个属性a
}
function C(a) { // 创建一个构造函数C
    if (a) { // 如果a存在
        this.a = a; // 给C的实例添加一个属性a
    }
}
A.prototype.a = 1; // 给A的原型添加一个属性a
B.prototype.a = 1; // 给B的原型添加一个属性a
B.prototype.b = 2; // 给B的原型添加一个属性b
C.prototype.a = 1; // 给C的原型添加一个属性a

console.log(new A()); // A {}
console.log(new B()); // B {a: undefined}
console.log(new C()); // C {}

console.log(new A().a); // 1  
console.log(new B().a); // undefined 
console.log(new B().b); // 2
console.log(new C(2).a); // 2

new A ().a:构造函数逻辑为空,返回的实例对象 _ proto_ 中包含了 a = 1 这个属性。new A ().a 时,发现实例对象本身没有 a,于是沿着原型链找到了原型中的 a,输出其值为 1。

new B ().a:构造函数中会无条件为实例对象创建一个自有属性 a,这个 a 的值以入参为准。这里我们的入参是 undefined,所以 a 值也是 undefined。

new C (2).a:构造函数中会有条件地为实例对象创建一个自有属性 a—— 若确实存在一个布尔判定不为 false 的入参 a,那么为实例对象创建对应的 a 值;否则,不做任何事情。这里我们传入了 2,因此实例输出的 a 值就是 2。

如果构造函数中定义了某个属性,在new调用的时候就要传入,否则会视为undefined,如果构造函数上没有定义则会按照原型链查找规则到原型链中查找。

2.5构造函数的工作机理

结合我们前面对构造函数的分析,当我们像这样通过 new + 构造函数创建新对象的时候:

function C(a) {
    if (a) {
        this.a = a;
    }
}

var c = new C(2)

实际上发生了四件事情:

  1. 为 c 实例开辟一块属于它的内存空间
  2. 将实例 c 的  _  proto_  这个属性指向构造函数 C 的 prototype 属性
  3. 把函数体内的 this 指到 1 中开辟的内存空间去
  4. 执行函数体内的逻辑,最后构造函数会帮你把创建的这个 c 实例 return 出来 耐心 细心梳理知识点 原型还与继承息息相关 可以看我这篇文章 点击这里

3.今日精进

在顺境中执着,在逆境中沉着——在顺境的情况下要坚持住保存本心,在不利条件下要冷静思考,理性思考解决之道。以积极乐观的态度面对人生,敢于面对顺境与逆境。

Day53【2022年9月15日】

学习重点: 原型原型链知识点补充 原型及原型链知识点补充

1.获得对象非原型链上的属性

使用后hasOwnProperty()方法来判断属性是否属于原型链的属性:

function iterate(obj){
   var res=[];
   for(var key in obj){
        if(obj.hasOwnProperty(key))
           res.push(key+': '+obj[key]);
   }
   return res;
} 

2.先有Object还是现有Function的问题

console.log(Function instanceof Object) // true
console.log(Object instanceof Function) // true

为什么会出现这种情况 至于这个问题可以参考这篇文章 这文章两篇文章讲的很详细 文章1 文章2

先有Object.prototype 再有Function.prototype 后再有 Function 和Object

首先:js中先创建的是Object.prototype这个原型对象。
然后:在这个原型对象的基础之上创建了Function.prototype这个原型对象。
其次:通过这个原型对象创建出来Function这个构造函数。
最后: 又通过Function这个构造函数创建了function Object()这个构造函数对象。 除了Object的原型对象(Object.prototype)的__proto__指向null 其他内置函数对象的原型对象和自定义构造函数的__proto__都指向Object.prototype 因为原型对象本身是普通对象

3.原型修改、重写

function Person(name) {
    this.name = name
}
// 修改原型
Person.prototype.getName = function() {}
var p = new Person('hello')
console.log(p.__proto__ === Person.prototype) // true
console.log(p.__proto__ === p.constructor.prototype) // true
// 重写原型
Person.prototype = {
    getName: function() {}
}
var p = new Person('hello')
console.log(p.__proto__ === Person.prototype)        // true
console.log(p.__proto__ === p.constructor.prototype) // false

constructor 属性返回 Object 的构造函数(用于创建实例对象)。注意,此属性的值是对函数本身的引用,而不是一个包含函数名称的字符串。

可以看到修改原型的时候p的构造函数不是指向Person了,因为直接给Person的原型对象直接用对象赋值时,它的构造函数指向的了根构造函数Object,所以这时候p.constructor === Object ,而不是p.constructor === Person。要想成立,就要用constructor指回来:

Person.prototype = {
    getName: function() {}
}
var p = new Person('hello')
p.constructor = Person
console.log(p.__proto__ === Person.prototype)        // true
console.log(p.__proto__ === p.constructor.prototype) // true

4.今日精进

每个人都有属于自身的处事道,道的标准取决于所接受的知识及个人认知。

Day54【2022年9月16日】

学习重点: 初识异步编程

1.什么是异步

同步,就是说后一个任务必须严格等待前一个任务执行完再执行,任务的执行顺序和排列顺序是高度一致的;异步,则恰恰相反,任务的执行顺序不必遵循排列顺序。比如说前一个任务就算没执行完,也没关系,先执行下一个任务就好,等前一个任务的执行结果啥时候出来了,我再把它临时穿插进来执行下。这其中,异步模式至关重要。大家知道,对我们前端来说,用户体验就是命。我们页面让用户苦等 2 分钟等一个表单提交的返回结果,同样是极不友好的一种交互体验。假如我们的主线程里,充斥着用户事件、ajax 任务等高耗时的操作,这种情况下还不采用异步方案,页面的卡顿甚至卡死将是不可避免的。

1.1 同步与异步实例

//同步代码
let x = 3; 
x = x + 4;
console.log(x);
//异步代码
setTimeout(function(){
   console.log("Hello World!");
}
, 1000);

2.异步历史及其分类

从整体上来说,异步方案经历了如下的四个进化阶段:

回调函数 —> Promise —> Generator —> async/await。

其中 Promise、Generator 和 async/await 都是在 ES2015 之后,慢慢发展起来的、具有一定颠覆性的新异步方案。相较于 “回调函数 “时期的刀耕火种而言,具有划时代的意义。

  • 回调函数 的方式,使用回调函数的方式有一个缺点是,多个回调函数嵌套的时候会造成回调函数地狱,上下两层的回调函数间的代码耦合度太高,不利于代码的可维护。
  • Promise 的方式,使用 Promise 的方式可以将嵌套的回调函数作为链式调用。但是使用这种方法,有时会造成多个 then 的链式调用,可能会造成代码的语义不够明确。
  • Generator 的方式,它可以在函数的执行过程中,将函数的执行权转移出去,在函数外部还可以将执行权转移回来。当遇到异步函数执行的时候,将函数执行权转移出去,当异步函数执行完毕时再将执行权给转移回来。因此在 generator 内部对于异步操作的方式,可以以同步的顺序来书写。使用这种方式需要考虑的问题是何时将函数的控制权转移回来,因此需要有一个自动执行 generator 的机制,比如说 co 模块等方式来实现 generator 的自动执行。
  • async 函数 的方式,async 函数是 generator 和 promise 实现的一个自动执行的语法糖,它内部自带执行器,当函数内部执行到一个 await 语句的时候,如果语句返回一个 promise 对象,那么函数将会等待 promise 对象的状态变为 resolve 后再继续向下执行。因此可以将异步逻辑,转化为同步的顺序来书写,并且这个函数可以自动执行。

3.今日精进

学习SA17讲,思考消化。细节与高度成反比;细节与全局成反比;细节与有效期成反比

Day55-Day56【2022年9月17日-9月18日】

外出

参考资料

  • JavaScript高级程序设计(第4版)
  • MDN
  • 解锁前端面试体系核心攻略
  • 鲨鱼哥面试题总结

结语

志同道合的小伙伴可以加我,一起交流进步,我们坚持每日精进(互相监督思考学习,如果坚持不下来我可以监督你)。我们一起努力鸭! ——>点击这里

备注

按照时间顺序倒叙排列,完结后按时间顺序正序排列方便查看知识点,工作日更新。