01、原型&继承

JS中的所有对象本质上都是通过**new Function()**创建出来的,包括字面量的{obj},也是new Object()的语法糖。每一个实例对象都有自己的原型,基于原型创建这个对象,Function本身也是一个对象。

1.1、obj.[[Prototype]]原型

JavaScript 常被描述为一种基于原型的语言 (prototype-based language)——每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推,这种关系常被称为原型链 (prototype chain)
🔵 obj.[[Prototype]] 原型:每个对象都有这个隐藏(不可访问)属性,他就是指向该对象的原型对象引用,也可以说是该对象的父级。
image.png

  • obj.proto(前后双下划线):设置、获取对象的原型。__proto__[[Prototype]]getter/setter访问器属性,是历史遗留下来的访问方式,不过还挺好用。 ```javascript let bird = {
    1. name: "bird",
    2. sayHi() { console.log(this.name + " hi!") },
    } let duck = {
    1. __proto__: bird, //设置原型__proto__
    } duck.proto = bird; //效果同上,设置原型proto

console.log(bird.name, duck.name) //bird bird bird.fly = function () { console.log(this.name + “ fly!”) } duck.name = “duck”; duck.fly(); //duck fly! //新鲜出炉的方法也被继承了

  1. ![](https://cdn.nlark.com/yuque/0/2022/jpeg/393451/1661617834555-ed54aebe-ebbf-41da-8c2e-145d67a5240d.jpeg)
  2. - Object.**getPrototypeOf**(obj)、Object.**setPrototypeOf**(obj,proto),是新加入的用于替代__proto__,用于获取、设置对象原型的方法。
  3. ```javascript
  4. const arr=[1,2];
  5. const t1=Object.getPrototypeOf(arr); //Array []
  6. const t2=Object.getPrototypeOf(t1); //Object { … }
  7. const t3=Object.getPrototypeOf(t2); //null
  8. console.log(t1,t2,t3);
  9. //获取对象的原型链
  10. function getPrototype(obj,arr=[]){
  11. if(obj===null){
  12. return arr;
  13. }
  14. const t=Object.getPrototypeOf(obj);
  15. arr.push(t);
  16. return getPrototype(t,arr);
  17. }
  18. console.log(getPrototype(1)); //Number Object { … } null
  19. console.log(getPrototype(true));//Boolean Object { … } null
  20. console.log(getPrototype("a")); //String Object { … } null

:::info ❗不要轻易更改原型,影响性能。当使用Object.**setPrototypeOf **obj.**__proto__**“即时”更改原型是一个非常缓慢的操作,因为它破坏了对象属性访问操作的内部优化。 :::

1.2、F.prototype继承

F.prototype 指的是构造函数**F** 的一个名为 “prototype” 的常规属性,指向一个原型对象——默认对象一个只有constructor(构造器)属性的对象,构造器constructor指向函数自身**F**
用构造函数**F()**创建新的对象时,构造函数里的属性每次都会重新创建,然后新对象会继承F.prototype,获得他的属性、方法财产。F.prototype可以被重写,可以修改(增、删除属性方法)。 :::warning 📢构造函数:就是一个函数,不过是为了创建对象用的。必须是function声明创建的函数:function FuncName(){ }

  • 所有属性、方法都赋值给this,没有return语句。
  • 约定大驼峰命名,用来区分普通函数。
  • 使用new F()来创建对象。这里new关键字的步骤:创建一个空对象;赋值this;执行构造函数中的代码,添加属性方法;返回新对象。 :::

    1. function Duck(name) {
    2. this.name = name;
    3. this.cry = function () { console.log(this.name + " cry!") };
    4. }
    5. let duck = new Duck("duck");
    6. console.log(Duck.prototype.constructor == Duck); //true
    7. console.log(duck.__proto__ == Duck.prototype); //true
    8. console.log(duck.constructor == Duck); //true

    JavaScript欲罢不能的对象原型与继承 - 图2
    🔸obj.constructor:对象构造器

  • 可以用对象的构造器来创建一个和该对象类似的新对象:new duck.**constructor**("kfc")

  • **F.prototype.constructor == F**:函数的prototype的构造器constructor等于他自己。

🔸new F():用构造函数F()创建对象,分配F.prototype到新对象的原型[[Prototype]]

  • F.prototype 只在new F()创建新对象是使用,设置为新对象的[[Prototype]]原型。F.prototype只支持对象、null,其他值会被忽略。
  • 如果F.prototype后面变更了,前后对象不影响,新的继承新的,旧的对象还是原有的。

    1. let bird = {
    2. name: "bird",
    3. fly: function () { console.log(this.name + " fly!") },
    4. }
    5. function Duck(name) {
    6. this.name = name;
    7. this.cry = function () { console.log(this.name + " cry!") };
    8. }
    9. Duck.prototype = bird; //原型继承,让new Duck()创建的实例对象都继承自bird,bird作为原型就是共享的
    10. Duck.prototype.type = "bird"; //增加原型属性
    11. let duck = new Duck("duck");
    12. duck.fly(); //duck fly!
    13. duck.cry(); //duck cry!
    14. console.log(Duck.prototype == bird); //true
    15. console.log(duck.__proto__ == bird); //true
    16. console.log(duck.constructor == Duck); //false

    如下图,由于bird实际上是由new Object()创建的,bird的构造函数就是Object()构造函数了。
    JavaScript欲罢不能的对象原型与继承 - 图3

    1.3、object万物之源

    JS中基本所有对象都继承自**Object**,准确的说是Object**.prototype**,Object.prototype的原型是**null**,算是继承的尽头。还有很多内置对象Array、Function等,每一个原型对象都内置了很多属性、方法。当我们创建这些对象时,就继承了他们的丰富财富。
    对于基本值类型稍有不同:

  • 值包装器:值类型String、NUmber、Boolean,只有数据值,不是对象,因此本身并没有什么属性、方法。当我们访问其属性、方法(如str.length)时,会产生一个临时的对象包装器对象,这个包装器对象就是基于其对应的String()、NUmber()、Boolean()构造器创建的。

  • null、undefined 没有对象包装器,也就么有任何属性、方法。

JavaScript欲罢不能的对象原型与继承 - 图4

  1. [1, 2, 3].__proto__ == Array.prototype; //true
  2. (() => { }).__proto__ == Function.prototype; //true
  3. (5).__proto__ == Number.prototype; //true
  4. let bird = { name: "bird" };
  5. let duck = { color: "red" };
  6. duck.__proto__ = bird; //继承bird
  7. console.log(duck.__proto__ == bird);//true
  8. console.log(duck.__proto__.__proto__ == bird.__proto__);//true
  9. console.log(duck.__proto__.__proto__ == Object.prototype);//true

:::warning 📢原型共享:(内置)原型也是可以修改的,也可以借用(复制),属性方法都存储在prototype 中(Array.prototype、Object.prototype)。原型prototype是全局共享的,需要注意! :::

  1. //判断是否存在
  2. if (!String.prototype.toInt) {
  3. //扩展原型的方法,转换数据类型为整数
  4. String.prototype.toInt = function (defaultValue = 0) {
  5. const num = parseInt(this);
  6. return num ? num : defaultValue;
  7. }
  8. }
  9. //借给(复制)给其他类型
  10. Number.prototype.toInt = String.prototype.toInt;
  11. "123a".toInt(); //123
  12. 123.11.toInt(); //123
  13. //扩展一个函数包装器defer,让任何函数延迟执行
  14. Function.prototype.defer = function (ms) {
  15. let f = this;
  16. return function (...args) {
  17. setTimeout(() => {
  18. f.apply(this, args);
  19. }, ms);
  20. }
  21. }
  22. console.log.defer(3000)("123a");
  23. alert.defer(2000)("Hi!");

1.4、到底继承了些什么东西?-原型链

继承是一层一层的,逐级往上,直到**Oject**(Object.prototype),形成了一个原型链。被继承的财富就藏在每一个原型上,当访问属性、方法时,先在自己内部查找,子类没有的属性/方法,会在原型链上向上查找,直到**null**(Object.prototype.proto),都没找到就返回undefined
image.png

  1. function Bird() {
  2. this.name = "bird";
  3. this.foods = [];
  4. this.eat = function (food) { this.foods.push(food) };
  5. }
  6. function Duck(name) {
  7. // super.name = name;
  8. this.color = "white";
  9. }
  10. Duck.prototype = new Bird(); //继承自Bird实例对象
  11. //修正constructor,不修正也没啥,就是别人用new duck.constructor("gaga")创建对象时不对
  12. Duck.prototype.contructor = Duck;
  13. Duck.prototype.fly = function () { console.log(this.name + " fly!") }
  14. let duck1 = new Duck();
  15. let duck2 = new Duck();
  16. console.log(duck1.__proto__.__proto__.__proto__ == Object.prototype);//true
  17. duck1.eat("rose");
  18. console.log(duck1.foods, duck2.foods); //['rose'] ['rose'] //共享属性foods,这不是我们想要的!
  19. Duck.prototype.name = "duck"; //在原型上修改值
  20. duck1.__proto__.name = "duck"; //效果同上
  21. console.log(duck1.name, duck2.name); //duck duck //都会生效,共享属性name
  22. duck2.name = "duck2"; //重新赋值属性值,不会影响原型
  23. console.log(duck1.name, duck2.name); //duck duck2 //duck2有自己的属性name值了
  24. duck2.foods.push("apple");
  25. console.log(duck1.foods, duck2.foods); //['rose', 'apple'] ['rose', 'apple'] //共享属性foods
  26. duck2.foods = ["私有food"];
  27. console.log(duck1.foods, duck2.foods); //['rose', 'apple'] ['私有food'] //duck2的私有foods

上面示例代码的原型链图:
JavaScript欲罢不能的对象原型与继承 - 图6
❗只能继承一个:一个对象只能继承一个原型对象,可以修改原型,会覆盖+有性能影响,尽量不这样做。
❓继承的财产在什么地方?

  • 对于具体对象,在obj.**__proto__**访问器属性上,实际是在obj.**[[Prototype]]**
  • 对于构造函数、内置的原型对象,财产都存在他们的构造器的prototype上,如F.prototype.prototypeArray.prototype

❓继承了些什么东西?——共享属性(使用共享,修改变私有)

  • 方法都继承了,技能都是靠血脉传承的,这个好理解。
  • 继承了所有属性,但属性的继承有一点特别,继承是单向的,可以用父类的属性&值,原型属性值变更后,所有实例对象的该属性值都跟着变,他们用的是同一个属性,属性是共享的。
  • 修改变私有:这个继承的属性只能看,不能模。你不能通过实例对象修改(重新赋值)原型上的属性值,如duck2.name = "duck2",当重新赋值时,会创建一个私有的同名属性,实际上是将属性添加到自己身上。
  • 属性值为引用对象:当修改引用对象内部数据时,并不影响共享,如duck2.foods.push("apple"),属性的引用地址并没有变更。

⁉️ 关于共享属性

  • 有时共享是需要的, 如统一型号的玩具,其基本属性如尺寸、颜色外观都是统一的,所有商品都共用即可,不用单独创建属性。
  • 有时不需要,如每一个用户都有自己的姓名、积分数量。不需要时怎么办呢,看看后面的实现继承的N中姿势!

image.png
🔵 怎么判断是不是亲生的?
判断属性是自己的,还是继承的。判断、获取自己的属性方法:

  • obj.hasOwnProperty(propName):判断是否自己的亲生的属性,返回bool值。
  • Object.keys(obj),获取obj自己的可枚举属性数组,不包含原型(父级)的属性。**for**(in)会循环所有的可枚举属性,包括原型链上的。

    1.5、实现继承的几种姿势

    贴心的JS为我们准备了N多种实现继承的姿势,体验丰富、欲生欲死、欲罢不能!了解前三个就基本可以了。
    JavaScript欲罢不能的对象原型与继承 - 图8

实现方式 优缺点
原型继承 手动设置原型链实现继承:
- subObj.__proto__ == parentObj
- Object.setPrototypeOf(obj , parentObj)
- SubFunc.prototype = parentObj
- Object.create(proto, propertiesObject)
🔴原型__proto__对象是共享的,大家共用一个属性值(特别是值为引用)
🔴无法向父类传递参数
借用构造函数 调用构造函数,借用其this的属性、方法,本质是复制,没有“继承”关系。
parentFunc.call(this)
🟢避免了属性共享,可以传递参数
🔴方法无法复用,每个实例对象都重新创建方法
组合继承 上面两种的组合:
- 借用构造函数:实现属性”继承“
- 原型继承:实现方法继承、复用
🟢实现了方法的重用,解决了属性共享
🔴至少调用两次父级构造函数?好像也不是什么大事
寄生组合 组合继承的改进版,添加了用一个空构造函数包装父级原型 🟢在组合继承基础上,减少了一次父类构造函数的调用。
🔴子类的原型被覆盖了
增强寄生组合 寄生组合的改进版,把子类原型中的属性手动加回来 🟢解决了上面的问题
class类继承 extends,属性并没有在class.prototype 🟢属性是私有的,方法是共享的,支持传参
  • 借用构造函数parentFunc.call(this),借鸡生蛋!

    1. function Bird() {
    2. this.type = "sam";
    3. this.hi = function () { console.log("hi") };
    4. }
    5. function Duck() {
    6. Bird.call(this);
    7. }
    8. let duck = new Duck();
    9. console.log(duck instanceof Bird); //false
    10. console.log(duck instanceof Duck); //true
    11. console.log(duck.hi == (new Duck()).hi); //false
  • 组合继承:借用构造函数:实现属性”继承“ + 原型继承:实现方法继承、复用 ```javascript function Bird(name) {

    1. this.name = name;
    2. this.colors = ["red"];

    } Bird.prototype.fly = function () { console.log(this.name + “ fly!”) }; Bird.prototype.type = “鸟类”; //需要共享的属性

function Duck(name) { Bird.call(this, name); //借用构造函数:实现属性”继承“ this.price = 100; } Duck.prototype = new Bird(); //原型继承:实现方法继承、复用 //修正constructor,不修正也没啥,就是别人用new duck.constructor(“gaga”)创建对象时不对 Duck.prototype.constructor = Duck; Duck.prototype.cry = function () { console.log(this.name + “ cry!”) }

let duck = new Duck(“sam”); console.log(duck instanceof Bird); //true console.log(duck instanceof Duck); //true console.log(duck.fly == (new Duck()).fly); //true duck.colors.push(“green”); console.log(duck.colors, new Duck(“ww”).colors); // [‘red’, ‘green’] [‘red’] //没有共享

  1. - **寄生组合式继承**:基本思路同组合继承,算是组合继承的改进版,直接设置子级的原型`F.prototype`,减少一次父级构造函数的调用。
  2. ```javascript
  3. function inherit(parentFunc, childFunc) {
  4. let SuperF = function () { }; //用一个空构造函数封装被继承的父级
  5. SuperF.prototype = parentFunc.prototype;
  6. childFunc.prototype = new SuperF(); //new 这个空构造函数,不用调用父级构造函数了。
  7. childFunc.constructor = childFunc;
  8. }
  9. //更粗暴的做法
  10. function inherit2(parentFunc, childFunc) {
  11. childFunc.prototype = parentFunc.prototype; //直接设置prototype
  12. childFunc.constructor = childFunc;
  13. }
  14. //父级
  15. function Bird(name) {
  16. this.name = name;
  17. this.colors = ["red"];
  18. }
  19. Bird.prototype.fly = function () { console.log(this.name + " fly!") };
  20. //子类
  21. function Duck(name) {
  22. Bird.call(this, name);
  23. this.price = 100;
  24. }
  25. Duck.prototype.cry = function () { console.log(this.name + " cry!") }; //被覆盖了
  26. inherit(Bird, Duck);
  27. let duck = new Duck("sam");
  28. console.log(duck instanceof Bird); //true
  29. console.log(duck instanceof Duck); //true
  30. console.log(duck.fly == (new Duck()).fly); //true
  31. duck.colors.push("green");
  32. console.log(duck.colors,new Duck("ww").colors); // ['red', 'green'] ['red'] //没有共享
  • 增强寄生组合:寄生组合的改进版,把子类原型中的属性手动加回来 ```javascript function inherit(parentFunc, childFunc) {
    1. let proto = parentFunc.prototype;
    2. //把子类原型的所有属性复制到一起
    3. Object.keys(childFunc.prototype).forEach(key =>
    4. Object.defineProperty(proto, key, { value: childFunc.prototype[key] }))
    5. childFunc.prototype = proto;
    6. childFunc.constructor = childFunc;
    } //父级 function Bird(name) {
    1. this.name = name;
    2. this.colors = ["red"];
    } Bird.prototype.fly = function () { console.log(this.name + “ fly!”) }; //子类 function Duck(name) {
    1. Bird.call(this, name);
    2. this.price = 100;
    } Duck.prototype.cry = function () { console.log(this.name + “ cry!”) }; inherit(Bird, Duck);

let duck = new Duck(“sam”); console.log(duck instanceof Bird); //true console.log(duck instanceof Duck); //true console.log(duck.fly == (new Duck()).fly); //true duck.colors.push(“green”); console.log(duck.colors, new Duck(“ww”).colors); // [‘red’, ‘green’] [‘red’] //没有共享

  1. ---
  2. <a name="sBfLy"></a>
  3. # 02、class类
  4. > JS终于有点像样的东西了——类Class。看完后之后:也就那样,坑也不少啊。
  5. **class**定义一个类型,可以更好的面向对象编程。class的本质上确实是函数,像构造函数的“语法糖”,构造器、原型继承基本都一样。不过他不是一般的语法糖,是JS内置的、有特殊标志的构造函数。
  6. | <br /> | **function 构造函数** | **class 类** |
  7. | --- | --- | --- |
  8. | **枚举属性** | 属性方法都可枚举 | 类方法不可枚举,默认enumerable = false,属性可以 |
  9. | **严格模式** | 默认模式 | 自动严格模式,`use strict` |
  10. | **提升** | 函数有提升效果,可先使用、后定义 | 不会提升 |
  11. | **使用方式** | 可以当函数使用 | 不能直接调用,只能new创建对象 |
  12. | **构造函数** | 就是函数本身 | 类中的`constructor()`函数,不申明也会自动生成 |
  13. | **命名方式** | 推荐大驼峰 | 大驼峰 |
  14. | **字段/属性** | 存在F.prototype中,是“共享”的 | 字段是被创建在实例对象中的,不在Class.prototype |
  15. | **语法** | 所有语句冒号`;`结尾。 | 方法申明不需要function关键字:`method(){}`<br />方法直接不加逗号`,`、冒号`;`其他同函数 |
  16. | **继承方式** | 设置__proto__Parent.Call() | extends |
  17. | **原型链** | `F.prototype` | function相同,多了类本身之间的继承(实现静态继承) |
  18. <a name="MYPA6"></a>
  19. ## 2.1、class基本语法
  20. ```javascript
  21. class ParentClass { }
  22. class MyClass extends ParentClass {
  23. //字段(属性)申明
  24. #name; //私有属性
  25. size = 100;
  26. // 构造器
  27. constructor(type) {
  28. super();
  29. this.type = type; //传统字段申明
  30. }
  31. // 方法
  32. method1() { }
  33. #method2() { } //私有方法
  34. //...
  35. //getter/setter
  36. get name() { }
  37. set name(value) { }
  38. }
  39. //使用new创建实例对象
  40. let obj = new MyClass(); //自动调用构造器方法创建对象
  • **class**申明一个类,类名 建议大驼峰命名,首字母大写。
  • **constructor**定义构造器函数,创建对象时默认调用constructor,可以没有(会自动创建)
  • 字段(属性)申明,构造器函数的写法。
  • 方法申明,**method**(para){},和函数申明略有不同。
  • 访问器属性**getters/setters**,同对象中的申明方式。
  • **extends**继承另一个类,可以继承自定义的类,也可以继承JS的原生类,如Array、Map,然后实现更多扩展。(不过原生类的静态属性方法不会被继承)
  • **supper**调用父级,在继承的内部可以通过supper调用父类的构造函数、属性方法。
  • **static**申明静态的属性、方法。
  • **#**私有属性、方法,#开头命名的字段、方法为私有的,不可外部访问,不可继承,肥水不流外人田。在这之前,大家都是约定下划线_开头命名,表示私有,哎,可怜的程序员! :::warning 📢 方法间没有逗号,、冒号;,其他语句同函数。
    方法默认是不可枚举的,默认enumerable = false,属性可以。 :::

    1. class User {
    2. constructor(name) { this.name = name; }
    3. sayHi() { console.log(this.name); }
    4. }
    5. let user = new User("sam");
    6. console.log(typeof User); // function
    7. console.log(user.__proto__ === User.prototype); // true
    8. console.log(user.constructor === User.prototype.constructor); // true
    9. console.log(user.constructor === User); // true
    10. console.log(User === User.prototype.constructor); // true

    上面是一个非常简单的类代码,实例对象user和类User的原型关系,同构造器函数是一样的,如下图。
    JavaScript欲罢不能的对象原型与继承 - 图9
    简写的class类表达式:同函数表达式写法。

    1. let Bird = class { name = "sam" };
    2. let Duck = class MyDuck { name = "sam" }

    2.2、extends继承

    类的继承同样遵从原型链的规则,都继承了Object原型,继承的子类可以重写父类的属性方法。

  • 📢**constructor**重写,必须调用父类的构造函数**supper()**

    • 如不重写构造函数,会自动生成并调用**supper()**
    • 为什么必须调用父类构造函数?子类是基于父类创建的,必须先构造父类,获得this对象,完成继承,再执行子类的构造函数,最终获得完成this的创建。
  • 方法重写,同名的方法会覆盖父类的方法,可以通过**super.method()** 来调用 父类的方法。
  • 字段重写,同方法,不过字段(属性)的重写很怪异的一点,😱,父类的构造函数总数调用自己的字段,而不是被重写的。
    • 为什么会这样?是由于奇特的执行顺序:先初始化父类字段 >> supper()执行父类构造函数 >> 初始化自己的字段 >> 执行自己的构造函数。
    • so,执行父类构造函数时,他还不知道自己的字段被绿了。
  • 单一继承/Mixin 模式:extends只能继承一个类。如果希望获得多个类的属性、方法,需要配合其他方式,如拷贝Object.assign。

    1. class Bird {
    2. #name;
    3. color = "red";
    4. constructor(name) {
    5. this.name = name;
    6. }
    7. cry() { console.log(this.name + " cry!") }
    8. get name() {
    9. return this.#name;
    10. }
    11. set name(value) { this.#name = value }
    12. }
    13. class Duck extends Bird {
    14. weight;
    15. constructor(name, weight) {
    16. super(name);
    17. this.weight = weight;
    18. }
    19. }
    20. let duck = new Duck("gaga", 10);
    21. console.log(Duck.__proto__ == Bird); //true 构造函数继承
    22. console.log(duck.__proto__ == Duck.prototype); //true
    23. console.log(duck.__proto__.__proto__ == Bird.prototype); //true
    24. console.log(duck.constructor == Duck); //true
    25. console.log(duck.__proto__.__proto__.constructor == Bird); //true
    26. duck.colors.push("yellow");
    27. console.log(duck.colors, new Duck().colors); //['red', 'yellow'] ['red'] 属性是私有的

    上面的代码中,类**Duck **继承自父类 **Bird**,extends 产生了两方面的原型继承,主要是多了类本身(构造函数)的继承

  • 构造函数继承(获得静态属性):类Duck继承自 类Bird,为构造函数之间继承,这样就继承了父类的静态函数、方法。

  • 原型继承Duck.prototype 继承自 Bird.prototype,这是对象实例继承的原型链。

JavaScript欲罢不能的对象原型与继承 - 图10 :::warning ⚠️箭头函数没有自己的this、supper

  • 复制有supper代码的方法是有风险的,可能会找不到supper?
  • 注意this、supper的丢失,例如通过setTimeout在另一个上下文环境中执行,可用箭头函数。 :::

    2.3、static静态属性方法

    static静态定义的方法属于这个类本身,不属于其任何实例,静态方法中的this也是指向的是类本身。通过类名进行调用,可以被继承。

  • 内部定义,static申明。

  • 外部赋值,通过类申明。
    1. class User {
    2. static Type = "VIP"; //内部用static申明定义静态属性、方法
    3. static showType() { console.log(this.Type); }
    4. }
    5. //外部定义静态属性、函数
    6. User.showType = function () { console.log(this.Type + 2); }
    7. User.Level = 99;
    8. //继承
    9. class SupperUser extends User {
    10. }
    11. console.log(User.Type, User.Level); //VIP 99
    12. User.showType(); //VIP2
    13. console.log(SupperUser.Type, SupperUser.Level); //VIP 99
    14. SupperUser.showType(); //VIP2

    2.4、判断数据类型方法

    |
    | 描述 | 返回值 | | —- | —- | —- | | typeof | 原始数据类型 | string | | {}.toString | 原始数据类型、内建对象,包含 Symbol.toStringTag 属性的对象 | string | | instanceof | 对象,会检测其原型链,只要在原型链上都返回true | true/false |
  1. class Bird {
  2. [Symbol.toStringTag] = "Bird"; //内置特殊属性[Symbol.toStringTag],自定义toString方法的值。
  3. }
  4. let bird = new Bird();
  5. console.log(typeof 1); //number
  6. console.log(typeof "1"); //string
  7. let ftype = Object.prototype.toString; //用最原始的toString方法
  8. ftype = {}.toString; //或者这样
  9. console.log(ftype.call(1)); //[object Number]
  10. console.log(ftype.call("1")); //[object String]
  11. console.log(ftype.call(ftype)); //[object Function]
  12. console.log(ftype.call({})); //[object Object]
  13. console.log(ftype.call([1, 2])); //[object Array]
  14. console.log(ftype.call(bird)); //[object Bird]
  15. console.log(bird instanceof Bird); //true
  16. console.log(bird instanceof Object); //true

03、GC内存管理

值类型变量的生命周期随函数,函数执行完就释放了。垃圾回收GC(Garbage Collection)内存管理主要针对引用对象,当检测到对象不会再被使用,就释放其内存。GC是自动运行的,不需干预也无法干预。
GC回收一个对象的关键就是——确定他确是一个废物,么有任何地方使用他了,主要采用的方法就是标记清理。

  • 标记清理(mark-and-sweep):标记内存中的所有的可达对象和他所有引用的对象),剩下的就是没人要的,可以删除了。
  • 引用计数:按变量被引用的次数,这个策略已不再使用了,由于回收垃圾的策略太垃圾从而被抛弃了。

🔸根(roots):当前执行环境最直接的变量,包括当前执行函数的局部变量、参数;当前函数调用链上的其他函数的变量、参数;全局变量。
🔸可达性(Reachability):如果一个值(对象)可以从根开始链式访问到他,就是可达的,就说明这个值还有利用价值。
JavaScript欲罢不能的对象原型与继承 - 图11
上图中FuncA函数中的局部变量 obj1,其值对象{P}存放在内存堆中,此时的值对象{P}被根变量obj1引用了,是可达的。

  • 如果函数执行完毕,函数就销毁了,变量引用obj1也一起随她而去。值对象{P}就没有被引用了,就不可达了。
  • 如果在函数中显示执行 obj1=null; 同样的值对象{P}没有被引用了,就不可达了。

image.png
GC定期执行垃圾回收的两个步骤:
① 标记阶段:找到可达对象并标记,实际的算法会更加精细。

  • 垃圾收集器找到所有的根,并“标记”(记住)它们。
  • 继续遍历并“标记”被根引用的对象。
  • …继续遍历,直到找到所有可达对象并标记。

② 清除阶段:没有被标记的对象都会被清理删除。 :::warning ⚠️全局变量不会被清理:属于window的全局变量就是根,始终不会被清理。有背景就是不一样! :::