由字节前端题引出的一串知识

102 阅读11分钟

八股先行

面试官:你对 js 中的 new 了解吗?

候选人:了解的,new 的作用是

  1. 在内存中创建一个新对象(作为实例对象)
  2. 根据原型链,设置 空对象的_proto_为构造函数的prototype(新对象内部的[[prototype]]指针被赋值为构造函数的prototype属性)
  3. 构造函数的this指向这个对象
  4. 执行构造函数的代码(给新对象增加属性)
  5. 返回该对象实例

回答完可能就问下一个问题了。

但这个八股的实际含义是什么,仅仅从背诵角度出发或许一知半解😶‍🌫️

一句话概括:new 运算符用于创建一个由构造函数定义的新对象,主要用于实例化对象

那么构造函数是什么?

实例化对象又是什么?

构造函数和对象实例

构造函数:是一种用于创建和初始化对象的特殊函数,它的主要作用是帮助创建具有相同结构和行为的对象实例。

构造函数有两种创建方法:

第一种是用 function 创建,第二是用 ES6 的 class 关键字创建

这时候就拿出经典的 Person 为例,这个构造函数可以创造 people(人们),而一个个人都是一个个对象,每个对象都有各自的 name 和 age

而这些对象也就是 Person 的实例

1 以函数的形式

    // function 创建
    function Person(name, age) {
        this.name = name;
        this.age = age;
        this.sayName = function () {
            console.log(this.name);
        };
    }

这是用 function 创建的最简单的构造函数,两个属性,一个方法

工厂模式

既然已经提到 Person() 构造函数,那么我们再讲一讲构造函数替代前的工厂模式

工厂模式:用于抽象创建特定对象的过程;说白了就是:工厂模式其实就是一个普通的函数,(这个函数)用于创建并返回一个新的对象,它显示的创建一个对象(就是这个 o),然后向这个对象添加属性和方法

    function createPerson(name, age) {
        let o = new Object(); // 显式的创建对象
        o.name = name;
        o.age = age;
        o.sayName = function () {
            console.log(this.name);
        };
        return o;
    }
    let person1 = createPerson("小新", 5);

tips:o 不仅可以使用 Object 构造函数还可以使用对象字面量,都可以方便的创建对象

上面的 createPerson 我们可以用不同的参数多次调用这个函数

特点

  • 相比于构造函数,不需要使用 new,直接使用函数名即可

  • 每次调用都会创建一个新的对象,每个对象都有自己的属性和方法

    • 自己的属性是指:自己独立的属性值,修改一个对象的属性值不会影响另一个对象
    • 自己的方法是指:每个对象实例会有自己独立的方法副本;每个对象都有自己的方法而不是共享同一个方法
  • 无法利用原型链共享方法,每个对象的方法都是独立的,无法共享,可能导致内存的浪费

构造函数模式

上文已经给出一种构造函数创建方法的方式了,下面再给出一种方式,方法定义在构造函数的 prototype 上(也就是该构造函数所创建的对象实例的原型

    function Person(name, age) {
        this.name = name;
        this.age = age;
    }
    Person.prototype.sayName = function () {
        console.log(this.name);
    };
    let person2 = new Person("小葵", 1);

特点

  • 没用显式的创建对象
  • 属性和方法直接赋值给了 this
  • 没用 return

person2 也拥有独立的 name 和 age,与工厂模式类似,修改一个实例的属性不会影响另一个实例

而 sayName 方法被定义在 Person.prototype 上,也就是说所有用 new Person 创建的实例共享同一个 sayName 方法

对比
特性工厂函数构造函数
创建方式普通函数调用,返回新对象使用 new 关键字调用,创建新实例
属性创建每个实例通过 o.property 创建独立属性每个实例通过 this.property 创建独立属性
方法创建每个实例都有自己独立的方法副本方法定义在 prototype 上,所有实例共享
内存使用每个实例有独立的方法,可能导致内存浪费方法共享,节省内存
this 绑定不依赖 this,显式创建和返回对象使用 this 指向新创建的实例
继承和扩展需要手动处理继承逻辑可以利用原型链更方便地实现继承和扩展
可读性和意图意图不明显,无法通过名称区分是否为工厂函数通过首字母大写等命名约定,明确表示为构造函数

2 以 class 的形式

类语法:

    class Person {
        constructor(name, age) {
            this.name = name;
            this.age = age;
        }
        sayName() {
            console.log(this.name);
        }
    }
    const person3 = new Person("美冴", 29);

特点:

  • 方法自动定义在 prototype 上,实现方法共享
  • 支持继承和其他面向对象的特性

3 对象实例

对象实例是指构造函数或类创建的具体对象

对象实例包含了类与构造函数定义的属性和方法

说白了:

  • 类或构造函数 是用来描述一类对象的模板或蓝图
  • 实例 是通过这个模板创建的具体对象,且有独立的属性值和行为方法

手写 new

言归正传,想真正理解 new 的作用还是得手写一下

首先来个构造函数 Person,具体一点

    function Person(name, age) {
        // this 会在这里指向由 new 创建的新对象
        this.name = name; // 将新对象的 name 属性设置为传入的 name 参数
        this.age = age;   // 将新对象的 age 属性设置为传入的 age 参数
    }
    const person = new Person("小新", 5);

先来点碎片找点感觉

  1. 创建一个新对象

    const newObject = {};

  2. 将新对象的原型指向构造函数的 prototype

    newObject.__proto__ = Person.prototype

  3. 将构造函数的 this 绑定到新对象上, 并执行构造函数的代码(想象一下,咔咔咔将构造函数内的属性传入新对象)

    Person.call(newObject, "小新", 5);

    还可以使用 apply,apply 传入的则是数组

  4. 返回新对象

    return newObject;

书接上文,就可以这样写:

好记版

    function myNew(constructor, ...args) {
        const obj = {};
        obj.__proto__ = constructor.prototype;
        const result = constructor.apply(obj, args);
        return result instanceof Object ? result : obj;
    }

这里的 return 需要注意,如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象

举个栗子🌰

    function Person(name) {
        this.name = name;
        return { obj: '返回的对象' };
    }
    ​
    const person = new Person('张三');
    console.log(person); // 输出: { obj: '返回的对象' }

但如果返回的是基本数据类型,则依然返回刚创建的对象

    function Person(name) {
        this.name = name;
        return '返回的对象';
    }
    ​
    const person = new Person('张三');
    console.log(person); // 输出: Person { name: '张三' }

微微升级版

    function myNew(constructor, ...args) {
        // 创建一个新对象,并将其原型指向构造函数的原型
        const obj = Object.create(constructor.prototype);
        const result = constructor.apply(obj, args);
        return result instanceof Object ? result : obj;
    }

Object.create 是 ES5 引入的一个方法,用于创建一个新对象,并将其__proto__指向指定的对象

基本语法是:

    Object.create(proto, [propertiesObject])

proto:新创建对象的原型对象

propertiesObject:[可选]为新创建的对象添加具有对应属性名称的属性描述符

字节前端题

我:这题有点难...

面试官:这题挺简单的

面试官:回去好好看一看这题

上题!

    var getName = function() { alert(4); };
    function Foo() {
        getName = function() { alert(1); };
        return this;
    }
    Foo.getName = function() { alert(2); };
    Foo.prototype.getName = function() { alert(3); };
    ​
    Foo.getName();
    getName();
    Foo().getName();
    getName();
    new (Foo.getName)();
    (new Foo()).getName();

怎么样你能写出来吗?可以在评论区输出🥰

敲黑板.webp

分析

下面开始分析:

首先我们看看当前的全局环境状态,包括各个变量、函数、方法的定义:

上半部分就有4个状态输入环境

定义和方法添加

首先定义全局变量、然后定义函数 Foo、再将静态方法添加到 Foo,最后给 Foo 的原型添加方法 getName

细说一下即

  1. 全局变量 getName

    var getName = function() { alert(4); };
    
    • 初始值:一个函数,执行时弹出 4
  2. 函数 Foo

    function Foo() {
        getName = function() { alert(1); };
        return this;
    }
    
    • 每次调用 Foo 时,会将全局变量 getName 重新赋值为新的函数,执行时弹出 1
    • 返回 this
  3. 静态方法 Foo.getName

    Foo.getName = function() { alert(2); };
    
    • 定义在构造函数 Foo 上的静态方法,执行时弹出 2
  4. 原型方法 Foo.prototype.getName

    Foo.prototype.getName = function() { alert(3); };
    
    • 定义在 Foo 的原型对象上,所以通过 new Foo() 创建的实例都能访问该方法,执行时弹出 3

静态方法和原型方法

静态方法原型方法是定义在类或构造函数上的两种不同类型的方法

它们的主要区别在于它们的调用方式和适用场景

在ES5中,静态方法就是直接赋值给构造函数的普通方法,没有 static 关键字,但可以实现类似静态方法的效果

静态方法和原型方法.png 而原型方法则可以使用.prototype
对比静态方法与原型方法
特性静态方法原型方法
定义位置类本身或构造函数上类的原型对象或构造函数的 prototype
调用方式通过类或构造函数调用通过实例对象调用
适用场景与实例无关的操作,例如工具函数或类级别的操作与实例相关的操作,需访问或操作实例的数据
访问权限只能通过类调用,无法通过实例调用只能通过实例调用,无法通过类调用
ES6 的类中
    class User {
        constructor(username, age) {
            this.username = username;
            this.age = age;
        }
    ​
        // 原型方法
        getInfo() {
            return `${this.username}, age: ${this.age}`;
        }
    ​
        // 静态方法
        static isAdult(age) {
            return age >= 18;
        }
    }
    ​
    const user = new User('小新', 5);
    ​
    // 调用原型方法
    console.log(user.getInfo()); // 输出: 小新, age: 5// 调用静态方法
    console.log(User.isAdult(35)); // 输出: true// 尝试错误调用
    console.log(user.isAdult(35)); // 报错,因为静态方法不能通过实例调用
    console.log(User.getInfo()); // 报错,因为原型方法不能通过类调用

总之原型方法就给实例调用,静态方法就给类调用

执行

下面开始下半部分的执行

  1. 执行 Foo.getName()

    • 作用:调用 Foo 的静态方法 getName
    • 执行:也就是调用 Foo.getName ,执行 function() { alert(2); }
    • 结果:弹出 2
  2. 执行 getName()

    • 作用:调用全局变量 getName,然后会指向那个函数
    • 执行:此时 getName 指向最初定义的函数 function () { alert(4); }
    • 结果:弹出 4
  3. 执行 Foo().getName()💥敲黑板💥

    • 作用:调用 Foo 函数,注意原来函数里返回了 this,再调用了返回值的 getName 方法

    • 执行

      1. 调用 Foo()

        • 在非严格模式下,this 指向全局对象(浏览器中是 window,node.js 中是 global
        • 函数内部执行 getName = function () { alert(1); };,此时会重新定义全局变量 getNamefunction () { alert(1); }
        • 返回 this(即全局对象 window
      2. 调用 window.getName()

        • 由于 Foo() 已经重新定义 getName,此时 window.getName()指向这个新的函数
    • 结果:弹出 1

  4. 再次执行 getName()

    • 作用:调用全局变量 getName 指向的函数
    • 执行:由于上一步,getName 已经被重新定义为 function () { alert(1); }
    • 结果:弹出 1
  5. 执行 new (Foo.getName)()💥💥猛敲黑板💥💥

    做好准备下面请听我娓娓道来...

    1. 首先我们解析括号内的表达式

      • Foo.getName 指向了我们的静态方法 function () { alert(2); }
    2. 再看 new,前文已经花了大量篇幅讲解 new

      • new 操作符用于创建一个新对象,并将新对象原型指向构造函数的 prototype

      • 执行 new (Foo.getName)()

        • 创建一个新对象,原型指向 Foo.getName.prototype ;tips:每个函数都有 prototype 属性
        • 调用构造函数 Foo.getName,相当于执行 function () { alert(2); },主要就是执行函数里面的
        • 函数没用返回值,new (Foo.getName)() 返回新创建的对象
    3. 结果:

      • 执行过程弹出 2
      • 返回一个新对象,该对象没有显示定义任何属性和任何方法;而且这个对象也没存😂
  6. 执行 (new Foo()).getName()💥💥再次猛敲黑板💥💥

    1. 还是先看括号内内容,new Foo()

      • 创建一个新对象(假设这个新对象叫 obj),obj 的 __proto__ 指向 Foo.prototype
      • 执行 Foo 构造函数,将 this 指向新创建的对象,这里就是执行 getName = function () { alert(1); },重新定义全局变量 getName
      • 返回 obj(这里的 this 指向的就是新对象)
    2. 调用 obj.getName()

      • obj 是 new Foo() 创建的对象,它的原型指向 Foo.prototype
      • Foo.prototype.getName = function () { alert(3); }
      • 所以,obj.getName() 会调用 Foo.prototype.getName,直接弹出 3

    这里涉及到了原型方法访问

    综上:

    Foo.getName(); → 弹出 2

    getName(); → 弹出 4

    Foo().getName(); → 弹出 1

    getName(); → 弹出 1

    new (Foo.getName)(); → 弹出 2

    (new Foo()).getName(); → 弹出 3

你做对了吗?

还可以想想 ES6 中是怎么样的

注意哦

  • ES5 构造函数:可以通过普通函数调用或使用 new 关键字调用。
  • ES6 class:只能通过 new 关键字调用,普通函数调用会抛出错误。

End

仅以此篇纪念我逝去的字节😭

与君共勉继续努力

如有错误欢迎探讨指正!❤️