刚开始学JavaScript的时候,很多人都会被“原型链”搞晕——明明自己写的对象里没这个属性,调用的时候咋就有了?修改原型上的方法,为啥所有实例都跟着变?原型链到底是咋运作的?今天咱们就把这事儿拆明白,从根儿上理解JavaScript原型链的工作逻辑。
先搞懂“原型”的基本角色
要理解原型链,得先分清构造函数、原型对象、实例这几个关键角色,以及它们之间的关联。
举个例子:用构造函数创建对象时,像这样写代码:
function Person(name) { this.name = name; // 给实例自身加属性 } const p1 = new Person('小明'); // 创建Person的实例p1
这里的 Person
是构造函数(本质是普通函数,用 new
调用时才当“构造器”用),每个构造函数都有个特殊属性 prototype
,它指向的对象就是原型对象——可以理解成“所有实例的公共模板”。
那实例和原型对象咋关联?每个实例(p1
)内部有个隐式原型 __proto__
(现在更推荐用 Object.getPrototypeOf()
来获取,更规范),它会“指向”构造函数的 prototype
,所以执行 Object.getPrototypeOf(p1) === Person.prototype
时,结果是 true
。
原型对象用来存啥?一般放公共属性或方法,比如所有 Person
实例都需要 sayHi
方法,就可以写到原型上:
Person.prototype.sayHi = function() { console.log(`我是${this.name}`); }; p1.sayHi(); // 输出“我是小明”
这么做的好处是节省内存——所有 Person
实例共享这个方法,不用每个实例都单独存一份。
原型链的“链条”是怎么串起来的?
原型链的核心逻辑是:每个对象的隐式原型(__proto__
)指向父级原型对象,父级原型对象的隐式原型又指向更上层的原型,直到终点 null
,就像糖葫芦一样,一个串一个,形成一条“链”。
还是用上面的 Person
例子,咱们一步步扒开这条链:
实例
p1
的__proto__
→Person.prototype
(构造函数的原型对象);Person.prototype
本身也是个对象,它的__proto__
→Object.prototype
(JavaScript里所有对象的“根原型”);Object.prototype
的__proto__
→null
(原型链的终点,没有更上层了)。
用代码验证更直观:
console.log(Object.getPrototypeOf(p1) === Person.prototype); // true console.log(Object.getPrototypeOf(Person.prototype) === Object.prototype); // true console.log(Object.getPrototypeOf(Object.prototype)); // null
再延伸到内置构造函数(比如数组),逻辑也一样:
const arr = [1,2,3]; console.log(Object.getPrototypeOf(arr) === Array.prototype); // true console.log(Object.getPrototypeOf(Array.prototype) === Object.prototype); // true console.log(Object.getPrototypeOf(Object.prototype)); // null
不管是自定义构造函数,还是JS内置的 Array
、Function
、Object
,它们的实例都会通过 __proto__
一层一层往上找,形成原型链。
原型链的“查找规则”是怎么运行的?
当我们访问一个对象的属性/方法时,JavaScript会触发「原型链查找」,规则是这样的:
先在对象自身找有没有这个属性/方法;
如果没找到,就去对象的隐式原型(
__proto__
)指向的原型对象里找;如果还没找到,就去原型对象的隐式原型里找(也就是父级原型);
以此类推,直到找到属性/方法,或者碰到原型链终点
null
(这时返回undefined
)。
举个“捉迷藏”的例子:
给 Person
原型和实例玩属性/方法的“捉迷藏”:
function Person(name) { this.name = name; } // 给原型加公共属性和方法 Person.prototype.age = 18; Person.prototype.sayHi = function() { console.log(`我是${this.name},今年${this.age}岁`); }; const p1 = new Person('小明'); const p2 = new Person('小红');
情况1:自身没有,原型有
console.log(p1.age); // 18(p1自身没age,去Person.prototype找) p1.sayHi(); // 我是小明,今年18岁(方法在原型上)
情况2:自身有,原型也有 → 自身优先
p1.age = 20; // 给p1自身加age属性 console.log(p1.age); // 20(自身有,不再去原型找) console.log(p2.age); // 18(p2自身没age,还是原型的18)
情况3:原型链上层也有
console.log(p1.toString()); // "[object Object]" // p1自身没有toString → 去Person.prototype找(没有)→ 再去Object.prototype找(有toString方法)
注意!赋值操作不走原型链
比如给 p1.age
赋值时,只是在p1自身添加属性,不会修改原型上的 age
,只有读取操作(console.log(p1.age)
)才会触发原型链查找。
原型链的终点为什么是null
?
前面例子里看到,Object.prototype.__proto__
的值是 null
,这是JavaScript的设计选择——null
表示“没有原型”,它是原型链的终点,用来终止查找过程。
试想如果没有这个终点会怎样?比如让 Object.prototype
的 __proto__
指向自己,那查找属性时就会无限循环,程序直接卡死,所以用 null
当终点,既能避免循环引用,又能明确“没找到”的边界。
可以把原型链想象成“家族族谱”:Object.prototype
是所有对象的“老祖宗”,再往上没长辈了,所以老祖宗的“父级”是 null
。
原型链在实际开发中有哪些用?
原型链看着抽象,实际开发中到处都有它的影子,最典型的场景是实现继承和共享公共逻辑。
(1)原型式继承
比如要创建 Student
构造函数,让它继承 Person
的属性和方法,可以通过修改原型链实现:
function Student(name, grade) { Person.call(this, name); // 继承“自身属性”(把name传给Person构造函数) this.grade = grade; // 学生特有的属性 } // 让Student的原型“指向”Person的原型,建立继承关系 Student.prototype = Object.create(Person.prototype); Student.prototype.constructor = Student; // 修正constructor的指向(否则会指向Person) const s1 = new Student('小李', '三年级'); s1.sayHi(); // 能调用Person原型上的sayHi方法
这里 Object.create(Person.prototype)
会创建一个新对象,它的 __proto__
指向 Person.prototype
,然后把 Student.prototype
设为这个新对象——这样 Student
的实例就能通过原型链访问到 Person
原型上的内容,实现“继承”。
(2)封装公共方法/属性
前面提过,把方法放到原型上,所有实例共享,能节省内存,比如React里的类组件,方法都是定义在原型上的;再比如数组的 push
、pop
等方法,其实都存在 Array.prototype
上,所有数组实例都能调用。
(3)扩展内置对象(谨慎使用)
有时候会看到给 Array.prototype
加自定义方法,
Array.prototype.myMap = function(callback) { const res = []; for(let i=0; i<this.length; i++){ res.push(callback(this[i], i, this)); } return res; }; [1,2,3].myMap(x=>x*2); // [2,4,6]
这种做法有争议(可能污染全局、和未来内置方法重名),但能帮我们理解:内置对象的实例能调用原型方法,靠的就是原型链。
容易踩的“原型链陷阱”有哪些?
原型链虽强,但稍不注意就会掉坑里,这几个场景要特别小心:
(1)直接替换原型对象,切断继承链
如果用字面量直接给构造函数的 prototype
赋值,会改变原型的指向,导致已有实例的原型链断裂:
function Person() {} const p1 = new Person(); // 直接替换prototype(危险操作!) Person.prototype = { sayHi() { console.log('Hi'); } }; const p2 = new Person(); p1.sayHi(); // 报错!p1的__proto__还是旧的Person.prototype(没有sayHi) p2.sayHi(); // 正常,p2的__proto__是新的原型对象
解决办法:如果要给原型加方法,逐个添加(Person.prototype.sayHi = ...
),而不是整体替换,如果必须替换,要手动修正原型链(但实际开发中很少这么做)。
(2)原型上的引用类型属性被共享
如果原型上放的是数组、对象这类引用类型,所有实例都会共享这个值,一个实例修改,其他实例也会受影响:
function Person() {} Person.prototype.friends = []; // 原型上放引用类型(数组) const p1 = new Person(); const p2 = new Person(); p1.friends.push('小张'); console.log(p2.friends); // ["小张"] → p2的friends也被改了
这是因为 p1.friends
读取时,找到原型上的 friends
数组(引用类型),push
操作修改的是这个数组本身。解决办法:把引用类型属性放到构造函数里(每个实例自己存一份),或者在实例初始化时深拷贝原型属性。
(3)误操作内置对象的原型
前面说过,给 Array
、Object
等内置对象的原型加方法,可能和未来新增的标准方法冲突,比如现在给 Array.prototype
加个 flatten
方法,过两年JS内置了 Array.prototype.flatten
,就会出现命名冲突,导致逻辑混乱,所以非必要不扩展内置原型。
原型链是JS的“继承骨架”
原型链的本质,是JavaScript实现继承和属性查找的核心机制,记住这几个关键点:
每个对象(除了
null
)都有__proto__
,指向父原型;属性查找时,会沿着
__proto__
一层一层往上找,直到null
;构造函数的
prototype
是实例的“模板”,原型链通过__proto__
串联;开发中要注意原型共享、原型替换等陷阱。
理解了原型链,再看JS里的类(class
语法,本质还是原型链)、继承、内置对象的方法调用,就会觉得顺理成章了,下次碰到“这个方法从哪来的”“修改原型为啥所有实例都变”这类问题,不妨从原型链的角度拆解,答案自然就出来啦~
网友评论文明上网理性发言 已有0人参与
发表评论: