一、什么是函数函数是一个“可执行的代码块”但在 JavaScript 中它本质上是一种特殊的对象Object它拥有普通对象没有的“内部属性”[[Call]]这就是它能被执行的原因。特殊之处1.可执行可以用 () 调用因为有 [[Call]] 内部槽。2.可挂载属性因为它是对象所以你可以给它挂属性fn.myName ‘test’。3.可被 new只要函数有 [[Construct]] 内部槽就能当构造函数使用。1.1 理解Object.prototype**Object.prototype 是所有 JavaScript 对象的根原型。**它是个普通对象里面装着 toString、hasOwnProperty 等所有对象都能用的通用方法。它的proto指向 null是原型链的终点指向null是为了终止整个链条避免无限循环。因此Object.prototype.proto为null无论你是普通对象、数组、函数还是 Date、RegExp最终都会通过proto链找到 Object.prototype从而继承这些通用方法。1.2 关于prototype 、proto、constructor 的表述1所有函数除了箭头函数都自有一个 prototype 对象。这个 prototype 对象里有一个自有的 constructor 属性以及一个从 Object.prototype 继承来的proto访问器属性。2函数本身作为对象还自有一个proto属性指向 Function.prototype并且通过这个proto继承了一个 constructor 属性指向 Function。总结1prototype函数才有箭头函数除外是一个对象存放共享数据即方法和属性。2proto所有对象都有指向自己的原型形成原型链接收prototype共享的数据。举例假设有类Ab是A的实例对象const b new A()那么 b.proto A.prototype3constructorprototype 对象上的属性指向构造函数本身。这三个属性的终极作用1.prototype 是为了让函数随时具备成为构造函数的能力是给实例提供共享方法的容器。2.proto是为了让函数作为对象时能继承通用方法如 call、apply。3.constructor 是为了让原型链保持双向完整支持 instanceof 等操作。二、函数分为哪几类按照定义方式、调用方式和内部槽[[Call]] / [[Construct]]我们分为以下五类2.1 第一类普通函数能否new可以能否 this 绑定动态绑定典型场景常规业务逻辑、构造函数它有 prototype 和proto并通过proto链从 Function.prototype 上继承了constructorfunctionsum(a,b){returnab;}console.log(sum(1,2));// 3console.log(sum.prototype);// { constructor: sum } 可作为构造函数// constructorconsole.log(sum.prototype.constructorsum);// true它们是同一个对象,sum.prototype.constructor 指向的是 sum 函数本身这个对象console.log(sum.prototype.constructor.name);// sum这才是函数名字符串/** sum.prototype.__proto__ 的值是 Object.prototype因为 sum.prototype 是一个普通对象它的原型指向 Object.prototype */console.log(sum.prototype.__proto__)functiongreet(){console.log(Hello);}// 1. 有 prototype作为构造函数身份console.log(greet.prototype);// { constructor: greet, __proto__: Object.prototype }// 2. 有 __proto__作为对象身份console.log(greet.__proto__);// Function.prototype// 3. 没有自己的 constructorconsole.log(greet.hasOwnProperty(constructor));// false// 4. 但可以访问到 constructor从原型链上找到的console.log(greet.constructor);// Functionconsole.log(greet.constructorFunction);// true// 因为 greet.__proto__ 指向 Function.prototype而 Function.prototype.constructor Function2.2 第二类箭头函数能否new不能无 prototype能否 this 绑定不能继承外层 this典型场景回调函数、setTimeout箭头函数没有 prototype 属性因为它不能被 new 调用但它有proto属性因为它是对象并且通过proto链从 Function.prototype 上继承了 constructor 属性。constarrow(){console.log(this);// 继承外层 this指向全局或外层上下文};arrow();console.log(arrow.prototype);// undefined 不能 new2.3 第三类方法函数能否new 不能除非是 class 方法能否 this 绑定绑定到所属对象典型场景对象行为、类原型方法constobj{name:JS,show(){// ES6 简写方法console.log(this.name);}};obj.show();// JS2.4 第四类构造函数/类能否new 必须用 new能否 this 绑定绑定到新创建实例典型场景面向对象编程、实例化functionDog(name){this.namename;}constdnewDog(旺财);console.log(d.name);// 旺财// 等价于 classclassCat{constructor(name){this.namename;}}2.5 第五类生成器函数能否new 不能next() 执行能否 this 绑定 动态绑定典型场景异步流程控制、迭代器function*gen(){yield1;yield2;return3;}constggen();console.log(g.next());// { value: 1, done: false }console.log(g.next());// { value: 2, done: false }console.log(g.next());// { value: 3, done: true }三、JavaScript 原型链继承机制prototype、proto、constructor这三者共同编织了原型链。当我们试图访问一个对象的属性时JavaScript引擎会首先在对象自身上查找。如果找不到它就会沿着该对象的proto 指针去其原型对象上查找。如果原型对象上依然没有它会继续沿着原型对象的proto向上查找如此往复直到原型链的顶端-Object.prototype。如果最终仍未找到则返回undefined。这种通过 proto链接起来的、自下而上的查找路径就是原型链继承的核心机制1、所有函数通过proto链先继承 Function.prototype获得 call、apply、bind而 Function.prototype 的proto又指向 Object.prototype所以函数最终也继承了 Object.prototype 的 toString、hasOwnProperty 等通用方法。– 所有函数都是 Function 的实例所以 函数.proto Function.prototype。– Function.prototype 本身是一个对象所以 Function.prototype.proto Object.prototype因此函数通过“串联继承”同时拥有了两套原型上的方法2、箭头函数和普通函数在继承链上没有任何区别。唯一的区别是箭头函数没有自己的 prototype 属性因为它不能被 new 调用。function Person() {} ┌─────────────────────────────────────────────────────────────────┐ │ Person 函数对象它本身 │ │ ├── Person.prototype ──────────────────────────────────────┐ │ │ ├── Person.__proto__ ────→ Function.prototype │ │ │ └── Person.constructor ──→ Function从 Function.prototype │ │ │ 继承来的 │ │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ Person.prototype 对象它是 Person 的“模具” │ │ ├── Person.prototype.constructor ──→ Person 函数本身自有 │ │ │ └── Person.prototype.__proto__ ────→ Object.prototype │ │ └─────────────────────────────────────────────────────────────────┘继承路径函数对象函数 → Function.prototype → Object.prototype → null实例对象实例 → 构造函数.prototype → Object.prototype → null普通对象对象 → Object.prototype → null箭头函数继承路径与普通函数完全一致。唯一区别是没有 prototype 属性不能作为构造函数。四、原型链相关方法4.1 hasOwnProperty ---- Object.hasOwn(obj, key)最新方案作用检查属性是否是对象自身的非继承应用场景for…in 循环中过滤原型属性判断对象是否有某个自有字段functionPerson(){}Person.prototype.name祖先;constpnewPerson();p.age18;console.log(p.hasOwnProperty(age));// true自己的console.log(p.hasOwnProperty(name));// false继承来的//在 for...in 循环中过滤掉继承的属性避免遍历到原型链上的方法。for(letkeyinp){if(p.hasOwnProperty(key)){console.log(key);// 只打印 age不会打印 name}}hasOwnProperty 的致命弱点 如果对象自身有一个名为 hasOwnProperty 的属性obj.hasOwnProperty 就会被覆盖导致调用失败。因此推荐使用Object.hasOwn(obj, key)最新方案constobj{name:小明,hasOwnProperty:我是冒名顶替的// 覆盖了原型上的 hasOwnProperty};console.log(Object.hasOwn(obj,name));// true依然正常工作console.log(Object.hasOwn(obj,age));// false4.2 Object.keys()作用获取对象自己的属性名列表应用场景遍历对象属性、将对象转换为数组、React 中渲染动态表单constobj{a:1,b:2};Object.prototype.hidden我是原型上的;console.log(Object.keys(obj));// [a, b]只返回自己的不包含继承的// React 中渲染动态表单constformData{name:小明,age:18};returnObject.keys(formData).map(key(Input key{key}label{key}value{formData[key]}/));4.3 Object.getOwnPropertyNames()作用返回对象自身所有属性名包括不可枚举的如 length应用场景调试、深度拷贝、需要获取 length 等不可枚举属性时constarr[1,2,3];console.log(Object.keys(arr));// [0, 1, 2]不包含 lengthconsole.log(Object.getOwnPropertyNames(arr));// [0, 1, 2, length]包含不可枚举的 length4.4 Object.getPrototypeOf()作用获取对象的原型推荐替代proto应用场景获取原型链、实现继承判断、调试functionPerson(){}constpnewPerson();console.log(Object.getPrototypeOf(p)Person.prototype);// trueconsole.log(Object.getPrototypeOf(p)p.__proto__);// true但推荐用前者4.5 Object.values(obj)作用获取对象自己的属性值列表constobj{a:1,b:2};console.log(Object.values(obj));// [1, 2]4.6 Object.entries(obj)作用获取 [key, value] 数组console.log(Object.entries(obj));// [[a, 1], [b, 2]]4.7 in 和 instanceofin检查属性是否存在于对象或其原型链上instanceof检查构造函数的 prototype 是否在对象的原型链上constobj{name:小明};console.log(nameinobj);// true自有console.log(toStringinobj);// true继承来的console.log(Object.hasOwn(obj,toString));// false区分自有/继承4.8 Object.create(null)Object.create(null) 会创建一个完全没有原型的对象。它的proto是 null因此它不继承 Object.prototype 上的任何方法比如 toString、hasOwnProperty、valueOf 等。constpureObjObject.create(null);console.log(pureObj.__proto__);// undefinedconsole.log(pureObj.toString);// undefinedconsole.log(pureObj.hasOwnProperty);// undefined为什么需要这样一个“纯净”的对象因为普通对象{}继承了很多默认方法这些方法可能会在特定场景下造成意想不到的副作用或安全风险。假设你写了一个函数用来把用户输入的 key 和 value 存储到一个对象里functionsetValue(obj,key,value){obj[key]value;returnobj;}constuserData{};setValue(userData,name,小明);setValue(userData,toString,恶意内容);// 用户把 key 设为 toStringconsole.log(userData.toString);// 恶意内容覆盖了原型上的 toString// 更危险的是如果用户传入 __proto__会污染所有对象的原型如果用户传入的 key 是proto或 constructor就可能污染全局原型链导致安全漏洞。使用 Object.create(null) 创建的对象没有原型因此即使被写入proto或 toString也不会影响任何其他对象因为它们只是作为普通键值对存在。Object.create(null) 的使用场景场景 1用作字典 / 缓存最常用constcacheObject.create(null);cache[user_123]{name:小明};cache[user_456]{name:小红};// 安全地检查 key 是否存在不会触发原型链上的意外属性if(cache[user_123]){console.log(cache[user_123]);// { name: 小明 }}2场景 2存储用户输入的数据防止原型污染functionsafeStore(userInput){conststoreObject.create(null);for(letkeyinuserInput){store[key]userInput[key];}returnstore;}constinput{name:小明,__proto__:{admin:true}};constsafeDatasafeStore(input);console.log(safeData.__proto__);// undefined安全性能敏感的映射表无原型链查找开销普通对象在访问属性时如果找不到会沿着原型链向上查找。Object.create(null) 没有原型链访问不存在的属性会立即返回 undefined没有额外的查找开销。constnormalMap{};constpureMapObject.create(null);console.time(普通对象);for(leti0;i1000000;i){normalMap[keyi]i;}console.timeEnd(普通对象);console.time(纯净对象);for(leti0;i1000000;i){pureMap[keyi]i;}console.timeEnd(纯净对象);使用 Object.create(null) 的注意事项1·它没有任何内置方法2.它无法通过 for…in 遍历原型链上的属性因为根本没有总结┌─────────────────────────────────────────────────────────────────┐ │ 原型链方法全图 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 查自有属性不查原型链 │ │ ├── Object.hasOwn(obj, key) ✅ 推荐ES2022 │ │ └── obj.hasOwnProperty(key) ⚠️ 旧写法可被覆盖 │ │ │ │ 遍历自有属性 │ │ ├── Object.keys(obj) 可枚举属性名 │ │ ├── Object.values(obj) 可枚举属性值 │ │ ├── Object.entries(obj) [key, value] 数组 │ │ └── Object.getOwnPropertyNames(obj) 含不可枚举属性 │ │ │ │ 操作原型链 │ │ ├── Object.getPrototypeOf(obj) 获取原型 │ │ ├── Object.create(proto) 创建对象并指定原型 │ │ └── Object.setPrototypeOf(obj, proto) ⚠️ 几乎不用 │ │ │ │ 类型判断 │ │ ├── obj instanceof Constructor 检查原型链 │ │ └── A.prototype.isPrototypeOf(B) 更直观的原型链检查 │ │ │ │ 检查是否存在含原型链 │ │ └── key in obj 不区分自有/继承 │ └─────────────────────────────────────────────────────────────────┘// 日常开发中最常用的 6 个操作constobj{name:小明,age:18};// 1. 判断是否自有Object.hasOwn(obj,name);// true// 2. 获取所有键Object.keys(obj);// [name, age]// 3. 获取所有值Object.values(obj);// [小明, 18]// 4. 遍历键值对Object.entries(obj).forEach(([key,value]){console.log(key,value);});// 5. 获取原型Object.getPrototypeOf(obj);// Object.prototype// 6. 判断类型objinstanceofObject;// true