【精华】为什么class在前端开发中不常用?

embedded/2025/3/1 19:17:45/

为什么class在前端开发中不常用?

  • js是一种基于原型的语言。
  • 它的对象继承是通过 原型链(prototype chain)实现的,每个对象都有一个 proto 属性指向它的原型。(大多数传统面向对象语言(如 Java、C++、Python、Ruby)是基于类的。在这些语言中,对象是通过类(class)定义的模板实例化出来的,继承是通过类之间的关系实现的。)
  • ES6 的 class 只是语法糖,底层还是基于原型。
class Student extends Person {constructor(name, age, grade) {super(name, age);this.grade = grade;}study() {return `${this.name} is studying`;}
}//实际上等价于:function Person(name, age) {this.name = name;this.age = age;
}
Person.prototype.sayHello = function() {return `Hello, my name is ${this.name}`;
};

为什么不像其他语言基于类?

这是一个非常有趣的问题!JavaScript 为什么选择基于原型而不是基于类,背后有历史、技术和设计哲学的原因。让我们一步步拆解。


1. JavaScript 的诞生背景

JavaScript 是由 Brendan Eich 在 1995 年为网景公司(Netscape)开发的,最初的目标是为网页添加简单的动态功能。当时的时间非常紧迫(据说只有 10 天设计和实现),因此语言设计上需要快速成型,同时保持简单和灵活。

在那个年代,面向对象编程(OOP)已经很流行,尤其是基于类的语言(如 C++ 和 Java)。网景公司的高层希望 JavaScript 能吸引 Java 开发者,所以表面上要求它“看起来像 Java”。但 Brendan Eich 本人更喜欢一种更动态、简洁的语言——他受到 Self 语言(一种基于原型的语言)的启发,最终选择了原型机制。


2. 为什么选择原型而不是类?

(1) 简单性和灵活性
  • 原型更简单
    基于类的语言需要定义类的蓝图(模板),然后通过实例化生成对象。这种机制在复杂系统中很强大,但在简单的脚本语言中显得繁琐。原型机制直接让对象可以“复制”或“继承”另一个对象的行为,不需要额外的类定义。例如:

    javascript">const person = {sayHello() {console.log("Hello");}
    };
    const alice = Object.create(person);
    alice.sayHello(); // "Hello"
    

    这比定义一个类、再实例化要直截了当。

  • 动态性
    原型允许运行时修改对象的结构。你可以随时给原型添加方法,所有基于这个原型的对象都会自动获得新行为:

    javascript">person.greet = function() {console.log("Hi there");
    };
    alice.greet(); // "Hi there"
    

    而基于类的语言通常是静态的,类定义一旦确定就很难改动。

(2) 适合脚本语言的轻量需求

JavaScript 最初是为网页设计的,主要用来操作 DOM、处理事件等简单任务。基于类的系统更适合大型、结构化的软件(如操作系统或企业级应用),但对于一个嵌入浏览器的小型脚本语言来说,原型机制更轻量、更灵活,不需要复杂的编译步骤。

(3) Self 语言的影响

Brendan Eich 深受 Self 语言启发。Self 是一种基于原型的语言,核心理念是“一切皆对象”,没有类的概念。对象直接通过克隆和修改原型来创建新对象。这种设计避免了类的抽象层,让语言更简洁。JavaScript 借鉴了这种思想,虽然加上了构造函数(function)来模拟类的实例化,但底层依然是原型。


3. 为什么不像其他语言基于类?

(1) 历史包袱和竞争压力
  • 在 1995 年,网景和微软(IE 的 JScript)在浏览器大战中竞争激烈。JavaScript 需要快速推出并赢得开发者支持。如果完全照搬 Java 的基于类设计,会增加语言的复杂度和实现成本,可能拖慢开发进度。
  • 同时,JavaScript 要吸引的不只是 Java 程序员,还有写脚本的非专业开发者(比如网页设计师)。原型机制加上动态特性,让它更容易上手。
(2) 与其他语言的目标不同
  • Java、C++ 等语言是为构建大型、静态类型系统设计的,基于类的机制适合严格的类型检查和复杂的继承体系。
  • JavaScript 的目标是动态性和交互性,运行在浏览器中,面对的是不可预测的网页环境。原型机制让它能快速适应变化,比如动态添加方法或属性。
(3) 哲学差异

基于类的语言(尤其是强类型语言)强调“提前规划”和“结构化”,而 JavaScript 的原型机制更倾向于“边做边改”和“自由度”。这反映了 Brendan Eich 想创造一种更自由、实验性语言的初衷。


4. 原型带来的独特优势

  • 灵活继承
    你可以直接操作原型链,实现多样的继承方式,而不像基于类的语言受限于单一继承(Single Inheritance)或复杂的多重继承。

    javascript">const obj1 = { a: 1 };
    const obj2 = Object.create(obj1);
    obj2.b = 2;
    console.log(obj2.a); // 1 (从原型继承)
    
  • 运行时扩展
    原型允许你在运行时动态修改对象的行为,这在浏览器环境中非常有用,比如 polyfill(填补浏览器功能的缺失)。

  • 简洁的实现
    原型的实现比类更轻量,占用内存更少,适合早期的资源有限的浏览器环境。


5. 为什么后来加了 class

尽管原型很强大,但开发者(尤其是来自 Java、C++ 背景的人)觉得它难以理解,而且代码组织不够直观。到了 ES6(2015 年),JavaScript 引入了 class 语法,主要是为了:

  • 迎合主流开发者的习惯。
  • 提供更清晰的面向对象编程方式。

但要注意,class 只是语法糖,底层仍然是原型机制。例如:

javascript">class Person {constructor(name) {this.name = name;}
}

实际上等价于:

javascript">function Person(name) {this.name = name;
}

总结:为什么 JS 基于原型?

  • 历史原因:快速开发、受到 Self 语言启发、迎合 Java 外观但保持简单。
  • 技术原因:原型更轻量、灵活,适合动态脚本语言的需求。
  • 哲学原因:强调自由度和运行时可变性,而不是静态结构。

如果 JavaScript 当初选择基于类,可能更像 Java,失去今天的灵活性和独特性,但也可能让初学者更难接受它的动态特性。原型机制是 JavaScript 的“灵魂”,也是它能适应从简单脚本到现代复杂应用的关键。

原型机制的弱点(如灵活性带来的不可预测性)被怎么解决了?

  • 渲染引擎的优化
  • TS
  • ES6 提供了class

在 TypeScript 中,直接修改原型(比如 xxx.prototype.somefunc = newFunc)并没有被完全禁止,但它的行为和可行性受到了一些限制和约束。这主要取决于上下文、类型检查的严格程度以及代码的组织方式。让我详细解释一下。


TypeScript已经不允许直接这样修改原型了吗?

TypeScript 对原型修改的态度

TypeScript 的核心目标是增强 JavaScript 的类型安全性和可维护性。它通过静态类型检查来减少运行时错误,但它仍然是 JavaScript 的超集,底层运行时行为没有改变。因此,TypeScript 允许你修改原型,但会通过类型系统和配置项对你施加一些限制。

1. 默认情况下是允许的,但类型不安全

你仍然可以在 TypeScript 中直接修改原型,比如:

function Person(name: string) {this.name = name;
}Person.prototype.sayHello = function() {console.log(`Hello, ${this.name}`);
};const p = new Person("Alice");
p.sayHello(); // 输出 "Hello, Alice"

这段代码在 TypeScript 中是合法的,也能正常运行。因为 TypeScript 不会阻止你操作 prototype,它只是试图为这种操作提供类型支持。

但是,TypeScript 的类型系统可能无法自动推断出你添加的 sayHello 方法,除非你显式声明它的类型。这会导致类型检查时的警告或错误。

2. 类型声明缺失的问题

如果你不告诉 TypeScript 你修改了原型,它会认为 sayHello 不存在:

const p = new Person("Alice");
p.sayHello(); // TS 错误: Property 'sayHello' does not exist on type 'Person'

要解决这个问题,你需要在 Person 的类型定义中显式声明这个方法,比如通过接口或类型扩展:

interface Person {name: string;sayHello(): void;
}function Person(this: Person, name: string) {this.name = name;
}Person.prototype.sayHello = function() {console.log(`Hello, ${this.name}`);
};const p = new Person("Alice");
p.sayHello(); // 正常运行,且类型安全
3. 使用 class 时更严格

如果你使用 TypeScript 的 class 语法(推荐的方式),直接修改原型会受到更多限制。因为 class 的方法和属性是静态定义的,TypeScript 会假设类的结构是固定的:

class Person {constructor(public name: string) {}
}Person.prototype.sayHello = function() {console.log(`Hello, ${this.name}`);
}; // 可以运行,但不推荐const p = new Person("Alice");
p.sayHello(); // TS 错误: Property 'sayHello' does not exist on type 'Person'

在这种情况下,TypeScript 不会自动识别 sayHello,因为它不在类的定义中。你需要通过接口扩展来声明:

interface Person {sayHello(): void;
}class Person {constructor(public name: string) {}
}Person.prototype.sayHello = function() {console.log(`Hello, ${this.name}`);
};const p = new Person("Alice");
p.sayHello(); // 现在类型安全
4. 严格模式下的限制

如果你在 tsconfig.json 中启用了严格模式(strict: true),尤其是 noImplicitAnystrictPropertyInitialization,TypeScript 会更严格地检查类型。直接修改原型而不声明类型会更容易触发错误,迫使你显式定义接口或类型。

5. 对内置对象原型的修改

如果你尝试修改内置对象的原型(如 Object.prototypeArray.prototype),TypeScript 不会阻止运行时行为,但会要求你通过声明合并(Declaration Merging)来扩展类型:

interface Array<T> {myCustomMethod(): void;
}Array.prototype.myCustomMethod = function() {console.log("Custom method on array");
};const arr = [1, 2, 3];
arr.myCustomMethod(); // 正常运行,且类型安全

不这样做的话,TypeScript 会报错,因为它不知道 myCustomMethod


TypeScript 为什么不完全禁止?

  1. 兼容 JavaScript
    TypeScript 是 JavaScript 的超集,必须兼容 JS 的所有特性,包括原型机制。如果直接禁止修改原型,会破坏与现有 JS 代码的兼容性。

  2. 动态性保留
    JavaScript 的动态特性(如运行时修改原型)是其核心优势之一,TypeScript 不想完全剥夺这种灵活性,而是通过类型系统引导开发者更安全地使用。

  3. 实际用例
    在一些场景中(比如 polyfill 或库开发),修改原型仍然是合法且必要的。TypeScript 允许这种操作,但要求你显式声明类型。


现代 TypeScript 的推荐实践

尽管你可以修改原型,TypeScript 更鼓励使用 class 或模块化的方式来组织代码,而不是直接操作 prototype

  • class 替代
    class Person {constructor(public name: string) {}sayHello() {console.log(`Hello, ${this.name}`);}
    }
    
  • 用模块和函数替代
    const createPerson = (name: string) => ({name,sayHello: () => console.log(`Hello, ${name}`)
    });
    

直接修改原型被视为“老派”或“不安全”的做法,尤其在大型项目中,可能导致:

  • 类型不一致。
  • 维护困难(团队成员难以追踪原型修改)。
  • 与静态类型系统的理念冲突。

结论

  • TypeScript 没有禁止修改原型,但通过类型系统和严格检查,间接限制了这种行为。如果你想这样做,需要显式声明类型,否则会报错。
  • 在现代开发中不推荐,因为 class 和函数式编程提供了更清晰、更安全的替代方案。
  • 如果你坚持用 xxx.prototype.somefunc = newFunc,是可以的,但要配合接口或类型声明,确保类型安全。

js中“复制”或“继承”的方法有哪些?

在 JavaScript 中,基于原型的“复制”或“继承”机制是其核心特性之一。由于 JavaScript 是基于原型的语言,它没有传统基于类语言中的“类继承”,而是通过原型链实现类似继承的功能。以下是 JavaScript 中实现“复制”或“继承”的常用方法,涵盖了从低级操作到高级语法的各种方式:


1. 通过 Object.create()

  • 作用:创建一个新对象,并将其原型设置为指定对象,实现继承。
  • 特点:直接操作原型链,简单且灵活。
  • 示例
    javascript">const parent = {sayHello() {console.log("Hello from parent");}
    };const child = Object.create(parent);
    child.sayHello(); // "Hello from parent"
    
  • 注意child 继承了 parent 的属性和方法,但自身是空的,可以添加新属性。

2. 通过构造函数和 prototype

  • 作用:利用构造函数和原型链实现继承,模拟类的行为。
  • 特点:传统方式,广泛用于 ES5 及之前。
  • 示例
    javascript">function Parent(name) {this.name = name;
    }
    Parent.prototype.sayHello = function() {console.log(`Hello, ${this.name}`);
    };function Child(name) {Parent.call(this, name); // 复制 Parent 的实例属性
    }
    Child.prototype = Object.create(Parent.prototype); // 继承原型方法
    Child.prototype.constructor = Child; // 修正 constructorconst child = new Child("Alice");
    child.sayHello(); // "Hello, Alice"
    
  • 步骤
    1. callapply 复制父构造函数的属性。
    2. Object.create 设置原型链。
    3. 修正 constructor 属性(可选)。

3. 通过 ES6 的 classextends

  • 作用:使用 ES6 的类语法实现继承(底层仍是原型)。
  • 特点:语法糖,更直观,适合现代开发。
  • 示例
    javascript">class Parent {constructor(name) {this.name = name;}sayHello() {console.log(`Hello, ${this.name}`);}
    }class Child extends Parent {constructor(name) {super(name); // 调用父类的构造函数}
    }const child = new Child("Alice");
    child.sayHello(); // "Hello, Alice"
    
  • 注意extends 底层是通过原型链实现的,等价于构造函数方式。

4. 通过对象字面量和扩展运算符(浅复制)

  • 作用:复制对象的属性(不涉及原型链),实现简单的“复制”。
  • 特点:不完全是继承,更像是属性拷贝,适用于简单场景。
  • 示例
    javascript">const parent = {name: "Alice",sayHello() {console.log("Hello");}
    };const child = { ...parent };
    child.name = "Bob";
    child.sayHello(); // "Hello"
    console.log(parent.name); // "Alice"(互不影响)
    
  • 限制:只复制自身属性,不复制原型上的方法,且是浅复制。

5. 通过 Object.assign()

  • 作用:将一个或多个源对象的可枚举属性复制到目标对象。
  • 特点:浅复制,常用于合并对象。
  • 示例
    javascript">const parent = {name: "Alice",sayHello() {console.log("Hello");}
    };const child = Object.assign({}, parent);
    child.name = "Bob";
    child.sayHello(); // "Hello"
    
  • 限制:和扩展运算符类似,不复制原型链。

6. 通过 __proto__(不推荐)

  • 作用:直接设置对象的 __proto__ 属性,指定原型。
  • 特点:低级操作,灵活但不推荐(性能差且已被废弃)。
  • 示例
    javascript">const parent = {sayHello() {console.log("Hello");}
    };const child = { __proto__: parent };
    child.sayHello(); // "Hello"
    
  • 警告__proto__ 是非标准特性,可能不支持,且不建议在生产代码中使用。

7. 通过混入(Mixin)

  • 作用:将多个对象的功能“混合”到一个对象中。
  • 特点:不依赖原型链,灵活实现多继承。
  • 示例
    javascript">const canSpeak = {sayHello() {console.log("Hello");}
    };
    const canWalk = {walk() {console.log("Walking");}
    };const person = Object.assign({}, canSpeak, canWalk);
    person.sayHello(); // "Hello"
    person.walk(); // "Walking"
    
  • 注意:属性冲突时,后者覆盖前者。

8. 深复制(特殊情况)

  • 作用:完全复制对象,包括嵌套属性,不涉及原型继承。
  • 特点:用于独立复制整个对象。
  • 示例
    javascript">const parent = {info: { name: "Alice" },sayHello() {console.log("Hello");}
    };const child = JSON.parse(JSON.stringify(parent));
    child.info.name = "Bob";
    console.log(parent.info.name); // "Alice"(互不影响)
    
  • 限制JSON 方法不支持函数和原型,且只适用于可序列化的数据。也可以用库(如 Lodash 的 _.cloneDeep)。

方法对比

方法类型是否涉及原型复制深度适用场景
Object.create继承-原型链继承
构造函数+原型继承-模拟类继承(ES5)
class extends继承-现代类继承(ES6+)
扩展运算符复制浅复制简单对象复制
Object.assign复制浅复制属性合并
__proto__继承-低级原型操作(不推荐)
Mixin复制/混合浅复制多功能组合
深复制(JSON)复制深复制完全独立对象

总结

  • 继承(基于原型)Object.createclass extends、构造函数+原型是主要方式,适合需要原型链的场景。
  • 复制(不基于原型):扩展运算符、Object.assign、深复制适用于属性拷贝,不涉及继承。
  • 混合方式:Mixin 用于组合多个对象的功能。

在现代 JavaScript 中,推荐使用 class extends(清晰且符合 OOP 习惯)或函数式方法(简单场景),避免直接操作低级原型(如 __proto__)。你有具体的需求吗?我可以帮你选个最适合的方法实现!

.__proto__和.prototype有什么区别?我已经完全混乱了,我也不太理解构造函数

别担心!我完全理解这些概念一开始可能会让人头脑一片迷雾,尤其是 __proto__prototype,还有构造函数的运作方式。我会用最简单的方式一步步解释,尽量让你理清思路。如果有任何地方还是不明白,可以随时打断我!


先从基础开始:什么是构造函数?

咱们先把构造函数弄清楚,因为它是 __proto__prototype 的基础。

构造函数的简单理解

构造函数就是一个普通的函数,但它的目的是通过 new 关键字来创建对象。它像一个“模板”,告诉你怎么造出一个新对象。

  • 例子
    javascript">function Person(name) {this.name = name; // 设置新对象的属性
    }const alice = new Person("Alice");
    // 相当于:/*1. const alice = {};2. alice.__proto__ = Person.prototype;3. Person.call(alice, "Alice");4. 返回 alice*/
    console.log(alice.name); // "Alice"
    
  • 发生了什么
    1. new Person("Alice") 创建了一个空对象 {}
    2. 把这个空对象的“内部指针”指向 Person.prototype(这就是 __proto__ 的作用,后面讲)。
    3. this 绑定到这个新对象上,执行函数体(给对象加了 name 属性)。
    4. 返回这个新对象,赋值给 alice
关键点
  • 构造函数本身只是个函数,new 让它变成“造对象”的工具。
  • 它会自动返回一个新对象(除非你手动返回其他东西)。

__proto__prototype 的区别

现在我们进入正题!这两个东西名字很像,但作用完全不同。

1. prototype(构造函数的属性)
  • 是什么prototype 是构造函数的一个属性(一个对象),它定义了所有通过这个构造函数创建的实例可以共享的属性和方法。

  • 谁有它:只有函数(尤其是打算用作构造函数的函数)有 prototype 属性。

  • 作用:当你用 new 创建对象时,新对象的“原型”会指向这个 prototype

  • 例子

    javascript">function Person(name) {this.name = name;
    }
    Person.prototype.sayHello = function() {console.log("Hello, " + this.name);
    };const alice = new Person("Alice");
    alice.sayHello(); // "Hello, Alice"
    
    • 这里 Person.prototype 是一个对象,里面放了 sayHello 方法。
    • alice 能调用 sayHello,因为它的“原型”指向了 Person.prototype
  • 小结prototype 是构造函数的“蓝图”,决定了实例能继承什么。

2. __proto__(对象的内部属性)
  • 是什么__proto__ 是每个对象都有的一个隐藏属性(内部指针),它指向这个对象的原型(也就是它继承的那个对象)。

  • 谁有它:所有对象(包括普通对象、数组、函数等)都有 __proto__

  • 作用:当你访问一个对象的属性或方法时,如果对象本身没有,JS 会通过 __proto__ 去原型上找。

  • 例子(接上面的代码):

    javascript">console.log(alice.__proto__ === Person.prototype); // true
    
    • alice.__proto__ 指向 Person.prototype,所以 alice 能用 sayHello
    • 如果你在 alice 上找不到某个属性,JS 会顺着 __proto__Person.prototype 找。
  • 小结__proto__ 是对象和它的原型之间的“连接线”。


用图来理清关系

假设有以下代码:

javascript">function Person(name) {this.name = name;
}
Person.prototype.sayHello = function() {console.log("Hello, " + this.name);
};
const alice = new Person("Alice");
  • 关系图
    Person (函数)|| .prototype (对象)|    ├── sayHello: function|    └── constructor: Person|
    alice (对象)|| .__proto__ 指向 Person.prototype|└── name: "Alice"
    
  • Person.prototypePerson 的属性,定义了共享的方法。
  • alice.__proto__alice 的内部指针,指向 Person.prototype

再加点例子巩固

示例 1:访问属性
javascript">function Dog(name) {this.name = name;
}
Dog.prototype.bark = function() {console.log("Woof!");
};const myDog = new Dog("Buddy");
console.log(myDog.name);    // "Buddy"(直接在 myDog 上)
console.log(myDog.bark());  // "Woof!"(从 myDog.__proto__ 找到)
console.log(myDog.__proto__ === Dog.prototype); // true
示例 2:原型链
javascript">const animal = {eat: function() {console.log("Eating...");}
};function Cat(name) {this.name = name;
}
Cat.prototype = animal; // 设置原型(继承 animal)const myCat = new Cat("Whiskers");
myCat.eat(); // "Eating..."(从 myCat.__proto__ 找到)
console.log(myCat.__proto__ === animal); // true

常见困惑解答

  1. 为什么我看不到 __proto__

    • 它是内部属性,不是直接定义的。你可以用 Object.getPrototypeOf(obj) 替代 __proto__,更标准:
      javascript">console.log(Object.getPrototypeOf(alice) === Person.prototype); // true
      
    • __proto__ 是历史遗留写法,不推荐直接用。
  2. 普通函数有 prototype 吗?

    • 是的,所有函数都有 prototype,但只有用作构造函数时才有用:
      javascript">function justFunction() {}
      console.log(justFunction.prototype); // 存在,是个对象
      
  3. 对象有 prototype 吗?

    • 没有!普通对象只有 __proto__,没有 prototype。只有函数才有 prototype

总结:proto vs prototype

属性谁拥有它作用示例值
prototype函数(构造函数)定义实例可以继承的属性和方法Person.prototype
__proto__所有对象指向对象的原型(连接原型链)alice.__proto__
  • prototype:是构造函数的“工具箱”,给所有实例共享用。
  • __proto__:是实例的“导航箭头”,指着它的原型。

再回到构造函数

构造函数的核心就是通过 new

  • 创建一个新对象。
  • 把新对象的 __proto__ 设为构造函数的 prototype
  • 初始化对象(用 this)。
  • 返回对象。

如果你还是觉得晕,我可以用更生活化的比喻(比如工厂和工人)再讲一遍,或者你告诉我具体哪里卡住了,我针对性解释!怎么样?有没有稍微清晰一点了?呜呜不要紧,我们慢慢来!

形象生动讲解

好的!这次我用一个更生活化、更形象的比喻来讲解 __proto__prototype,还有构造函数的运作方式。想象我们在一个“魔法玩具工厂”里,目标是制造会说话的小机器人玩具。让我们一步步走进这个场景,把这些概念变得生动起来!


场景设定:魔法玩具工厂

  • 你是工厂的老板,想批量生产会说话的小机器人。
  • 每个机器人都有自己的名字(个性化属性),还能说“Hello”(共享技能)。
  • 工厂里有一些工具和角色,我们用它们来解释构造函数、prototype__proto__

1. 构造函数:工厂的“机器人制造机”

  • 比喻:构造函数是一个“机器人制造机”。你把原材料(比如名字)放进去,按下“启动”(new),它就吐出一个新的机器人。

  • 形象化

    • 机器的名字叫 Person
    • 你输入“Alice”作为原料,按下“启动”,机器就造出一个名叫 Alice 的机器人。
  • 代码

    javascript">function Person(name) {this.name = name; // 给新机器人贴上名字标签
    }const alice = new Person("Alice"); // 按下“启动”按钮
    console.log(alice.name); // "Alice"
    
  • 发生了什么

    1. 工厂启动,制造机创建一个空壳机器人({})。
    2. 机器给这个空壳贴上“Alice”的名字标签(this.name = name)。
    3. 最后把成品机器人交给你(返回对象)。
  • 关键点:这个“制造机”(Person)本身不会说话,但它知道怎么给机器人装上零件。它还有个秘密武器——“技能蓝图”(这就是 prototype)。


2. prototype:技能蓝图

  • 比喻prototype 是工厂里的“技能蓝图”,一张写着“所有机器人都会的技能”的图纸。制造机(Person)会把这张图纸交给每一个新机器人,让它们学会蓝图上的技能。

  • 形象化

    • 你在蓝图上写下:“所有机器人都会说 Hello”。
    • 每个机器人出厂时,都会带上这张蓝图的“使用权”。
  • 代码

    javascript">Person.prototype.sayHello = function() {console.log("Hello, " + this.name);
    };const alice = new Person("Alice");
    alice.sayHello(); // "Hello, Alice"
    
  • 发生了什么

    • Person.prototype 是制造机的一个特殊抽屉,里面放着蓝图。
    • 蓝图上写着 sayHello 的技能(一个函数)。
    • 每个机器人(比如 alice)出厂时,制造机会偷偷告诉它:“如果你需要技能,去我的蓝图上看。”
  • 关键点

    • prototype 是制造机(构造函数)的财产,不是机器人自己的东西。
    • 所有用 Person 造出来的机器人都共享同一张蓝图,节省工厂资源。

3. __proto__:机器人的“技能导航仪”

  • 比喻__proto__ 是每个机器人身上装的一个小导航仪,告诉它:“如果你自己不会什么技能,就去蓝图(prototype)那儿找。”

  • 形象化

    • 小机器人 Alice 想要说“Hello”,但它自己没装这个技能。
    • 它打开导航仪(__proto__),导航仪指向工厂的蓝图(Person.prototype)。
    • 在蓝图上找到 sayHello,然后用自己的名字喊出来。
  • 代码

    javascript">console.log(alice.__proto__ === Person.prototype); // true
    alice.sayHello(); // "Hello, Alice"
    
  • 发生了什么

    • alice 是工厂造出来的机器人,它的 __proto__ 导航仪指向 Person.prototype
    • 当你叫 alice.sayHello() 时:
      1. Alice 发现自己没这个技能。
      2. 它用导航仪查到蓝图(Person.prototype)。
      3. 在蓝图上找到 sayHello,然后用自己的名字(this.name)执行。
  • 关键点

    • __proto__ 是机器人(对象)自己的东西,指向它的“出身蓝图”。
    • 它像一根绳子,把机器人和工厂的蓝图连起来。

工厂的完整运作流程

  1. 老板下单const alice = new Person("Alice")
    • 工厂启动制造机(Person),输入原料“Alice”。
  2. 制造机器人
    • 制造机造一个空壳({})。
    • 给空壳装上名字“Alice”(this.name = name)。
    • 把导航仪(__proto__)调到指向蓝图(Person.prototype)。
  3. 交付:成品机器人 alice 出厂,能用蓝图上的技能(sayHello)。
  • 结果
    • alice.name 是它自己的零件(“Alice”)。
    • alice.sayHello 是通过导航仪从蓝图借来的技能。

再加个场景:继承

  • 比喻:工厂升级了,想造一种会跑的机器人(Runner),它继承了普通机器人的技能。

  • 代码

    javascript">function Runner(name) {Person.call(this, name); // 借用 Person 的零件
    }
    Runner.prototype = Object.create(Person.prototype); // 复制蓝图
    Runner.prototype.run = function() {console.log(this.name + " is running");
    };const bob = new Runner("Bob");
    bob.sayHello(); // "Hello, Bob"(从 Person 的蓝图)
    bob.run();      // "Bob is running"(自己的蓝图)
    
  • 形象化

    • Runner 是新款制造机,它先借用 Person 的零件(名字)。
    • 它复制了 Person 的蓝图(Object.create),然后加了自己的技能“跑步”。
    • bob 的导航仪(__proto__)指向 Runner.prototype,而 Runner.prototype 的导航仪又指向 Person.prototype

总结:角色分工

  • 构造函数(Person:工厂的制造机,负责造机器人。
  • prototype:制造机的技能蓝图,写着共享技能(sayHello)。
  • __proto__:每个机器人的导航仪,指向它的蓝图。
生活化对比
概念工厂比喻作用
构造函数制造机造出新机器人,装上零件
prototype技能蓝图定义所有机器人能共享的技能
__proto__导航仪告诉机器人去哪找技能

检查一下清楚了吗?

  • 想象 alice 是一个小机器人,它的名字是“Alice”,它不会说话,但能通过导航仪(__proto__)找到工厂蓝图(Person.prototype)上的 sayHello
  • 如果你问 alice:“你是谁造的?”它会说:“Person 制造机造的,我的技能蓝图在它那儿!”

还有哪里觉得模糊吗?或者想让我再举个例子(比如造个会跳舞的机器人)?这次有没有更形象一点呀?

Person.call(this, name) 什么意思?

  • Person.call(this, name);其实就是运行这个Person机器,但是用我给你的“this”来生产对吧。
  • 对!完全正确!

Person.call(this, name) 的作用

  • 比喻:Person.call(this, name) 就像你拿起电话,打给“Person 制造机”,说:“嘿,我这儿有个空壳机器人(this),你帮我给它装上名字零件(name),但别按你的完整流程走,我只借你装零件的步骤!”
  • 形象化:
    “Runner 制造机”启动,造出一个空壳机器人(比如 bob)。
    它把这个空壳(this)交给“Person 制造机”,说:“给它装上名字‘Bob’。”
    “Person 制造机”执行它的逻辑(this.name = name),把名字装到空壳上,然后完事。

call 、bind、apply

好的!让我来详细介绍一下 call 方法,顺便带上你提到的 bind 方法,以及它们跟 apply 的关系(因为这三兄弟经常一起出现)。我会尽量用生动的方式解释,结合我们之前的“魔法玩具工厂”比喻,让你轻松理解它们的用途和区别!


背景:函数的“魔法按钮”

在 JavaScript 中,函数不仅仅是代码块,它们还有一些魔法按钮(方法),可以改变函数的执行方式。这些方法绑定在所有函数上,来自 Function.prototype,包括 callapplybind。它们的核心作用是控制函数中的 this 和参数


1. call 方法

是什么?
  • call 是一个函数方法,可以让你手动指定函数中的 this 指向,并且立即执行这个函数。
  • 语法function.call(thisArg, arg1, arg2, ...)
    • thisArg:你想让 this 指向的对象。
    • arg1, arg2, ...:传递给函数的参数(逐个列出)。
比喻:工厂的“电话借用服务”
  • 想象你在“Runner 制造机”旁边,有个电话。你拨通“Person 制造机”(Person 函数)的号码,说:“我这儿有个空壳机器人(this),你帮我执行你的逻辑,把名字装上吧!”
  • “Person 制造机”接到电话,用你的空壳(指定的 this)执行它的代码。
示例
javascript">function Person(name) {this.name = name;console.log("Person says: My name is " + this.name);
}function Runner(name) {Person.call(this, name); // 借用 Person 的逻辑
}const bob = new Runner("Bob"); // "Person says: My name is Bob"
console.log(bob.name); // "Bob"
  • 发生了什么
    • Runner 造了个空壳(this{})。
    • call 打电话给 Person,说:“用我的空壳,装上名字‘Bob’。”
    • Person 执行,把 this.name = "Bob" 装到 bob 上。
用处
  • 借用功能:像上面这样,复用其他函数的逻辑。
  • 控制 this:在普通调用中,this 可能指向 windowundefined(严格模式),call 让你指定它。

2. bind 方法

是什么?
  • bind 也是函数方法但它不会立即执行函数,而是返回一个新函数,这个新函数的 this 被永久绑定到你指定的对象。
  • 语法function.bind(thisArg, arg1, arg2, ...)
    • 返回一个新函数,this 固定为 thisArg
    • 可以预先绑定部分参数(柯里化)。
比喻:工厂的“遥控器定制”
  • 想象你不想每次都打电话给“Person 制造机”,而是定制一个遥控器(新函数)。这个遥控器已经设定好:无论在哪按下按钮,this 永远指向你指定的机器人。
  • 你拿着遥控器,按下按钮时才会执行。
示例
javascript">function Person(name) {this.name = name;console.log("Hello, " + this.name);
}const robot = { name: "Robo" };
const boundPerson = Person.bind(robot); // 返回新函数,this 绑定到 robot
boundPerson("Alice"); // "Hello, Robo"(name 参数被忽略,因为 this 已绑定)
  • 发生了什么
    • bind 创建了一个新函数 boundPerson,它的 this 永久指向 robot
    • 调用时,this.name 用的是 robot.name,传入的 “Alice” 没起作用(除非函数逻辑用参数)。
用处
  • 固定 this:比如事件监听器中,防止 this 乱跑:
    javascript">const obj = {name: "Obj",say: function() {console.log(this.name);}
    };
    const boundSay = obj.say.bind(obj);
    setTimeout(boundSay, 1000); // "Obj"(不绑定的话,this 可能是 window)
    
  • 预设参数:提前锁定部分参数:
    javascript">function add(a, b) {return a + b;
    }
    const add5 = add.bind(null, 5); // this 无关,绑定 a=5
    console.log(add5(3)); // 8
    

3. apply 方法(顺便介绍)

是什么?
  • applycall 很像,也是立即执行函数并指定 this,但参数以数组形式传递。
  • 语法function.apply(thisArg, [args])
    • thisArg:指定的 this
    • [args]:参数数组。
比喻:工厂的“批量订单”
  • 你还是打电话给“Person 制造机”,但这次不是一个个零件报过去,而是递过去一张清单(数组),说:“按这个清单装零件!”
示例
javascript">function Person(name, age) {this.name = name;this.age = age;
}function Runner(name, age) {Person.apply(this, [name, age]); // 用数组传递参数
}const bob = new Runner("Bob", 25);
console.log(bob.name); // "Bob"
console.log(bob.age);  // 25
  • 发生了什么
    • apply[name, age] 展开,像 this.name = name; this.age = age 一样执行。
用处
  • 处理数组参数:比如找最大值:
    javascript">const numbers = [1, 5, 3, 8];
    console.log(Math.max.apply(null, numbers)); // 8
    

三兄弟对比

方法执行时机参数形式返回值比喻
call立即执行逐个参数函数执行结果电话借用服务
apply立即执行参数数组函数执行结果批量订单清单
bind不执行,返回新函数逐个参数(可预设)新函数定制遥控器
图解工厂场景
  • call:你喊:“Person,帮我装名字‘Bob’,现在就干!”(马上装好)
  • apply:你递清单:“Person,按 [‘Bob’, 25] 装零件,现在开工!”(马上装好)
  • bind:你定制遥控器:“Person,锁定这个机器人,以后我按按钮你就装名字。”(等你按)

回到你的问题

  • Person.call:我们之前用它让“Runner 制造机”借用“Person”的装名字功能,直接把名字装到 bob 上。
  • bind:如果你用 Person.bind(this)(name),会返回一个绑定了 this 的新函数,但不会立即执行,得再调用一次。
对比代码
javascript">function Person(name) {this.name = name;
}function Runner(name) {Person.call(this, name); // 立即装名字
}function RunnerBind(name) {const bound = Person.bind(this); // 只定制,不执行bound(name); // 手动再按一下按钮
}const bob = new Runner("Bob");      // "Bob"(call 直接生效)
const bob2 = new RunnerBind("Bob"); // "Bob"(bind 需两步)

总结

  • call:借用功能,马上干活,适合继承零件(像 RunnerPerson)。
  • bind:定制一个新函数,随时用,常用于固定 this 或预设参数。
  • 额外福利 apply:和 call 类似,但爱用数组传参。

http://www.ppmy.cn/embedded/169133.html

相关文章

DeepSeek行业应用实践报告-智灵动力【112页PPT全】

DeepSeek&#xff08;深度搜索&#xff09;近期引发广泛关注并成为众多企业/开发者争相接入的现象&#xff0c;主要源于其在技术突破、市场需求适配性及生态建设等方面的综合优势。以下是关键原因分析&#xff1a; 一、技术核心优势 开源与低成本 DeepSeek基于开源架构&#xf…

【PyQt5】python可视化开发:PyQt5介绍,开发环境搭建快速入门

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

Prompt Engineering for Large Language Models

题目 大型语言模型的快速工程 简介 随着 OpenAI 的 ChatGPT 和 Google 的 Bard 等软件的普及&#xff0c;大语言模型&#xff08;LLM&#xff09;已经渗透到生活和工作的许多方面。例如&#xff0c;ChatGPT 可用于提供定制食谱&#xff0c;建议替换缺失的成分。它可用于起草研…

服务器IPMI用户名、密码批量检查

背景 大规模服务器部署的时候&#xff0c;少不了较多的网管和监测平台&#xff0c;这些平台会去监控服务器的性能、硬件等指标参数&#xff0c;为了便于管理和控制&#xff0c;则需要给服务器IPMI带外管理添加较多的用户&#xff0c;这就需要对较多的服务器检查所对应的IPMI用…

基于 Python 的天气数据分析与可视化

基于 Python 的天气数据分析与可视化 1. 项目背景 天气数据分析与可视化项目旨在通过爬取天气数据并进行分析&#xff0c;生成可视化图表&#xff0c;帮助用户了解天气变化趋势。通过该项目&#xff0c;学生可以掌握 Python 的数据爬取、数据分析和可视化技能。该项目适用于气…

【Linux系统】—— 冯诺依曼体系结构与操作系统初理解

【Linux系统】—— 冯诺依曼体系结构与操作系统初理解 1 冯诺依曼体系结构1.1 基本概念理解1.2 CPU只和内存打交道1.3 为什么冯诺依曼是这种结构1.4 理解数据流动 2 操作系统2.1 什么是操作系统2.2 设计OS的目的2.3 操作系统小知识点2.4 如何理解"管理"2.5 系统调用和…

vue的双向绑定是怎么实现的

Vue.js 的双向绑定是通过 数据劫持&#xff08;Data Observation&#xff09; 和 发布-订阅模式&#xff08;Publish-Subscribe Pattern&#xff09; 实现的。具体来说&#xff0c;Vue 使用了以下核心技术&#xff1a; 数据劫持&#xff1a;通过 Object.defineProperty 或 Prox…

SQL打折日期交叉问题

1. 数据结构&#xff1a;数据为平台商品促销数据 表名&#xff1a;good_promotion 字段名&#xff1a;brand&#xff08;品牌&#xff09;、stt&#xff08;打折开始日期&#xff09;、edt&#xff08;打折结束日期&#xff09;。 2. 需求&#xff1a; ① 创建表 ② 计算每个…