├── .gitattributes ├── src ├── index.d.ts └── index.js ├── .npmrc ├── .github ├── dependabot.yml └── workflows │ ├── pull_request.yml │ └── main.yml ├── benchmark ├── package.json ├── README.md └── index.js ├── .editorconfig ├── .gitignore ├── CHANGELOG.md ├── test └── index.js ├── LICENSE.md ├── package.json └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | declare const NullPrototypeObject: { 2 | new (): any; 3 | }; 4 | 5 | export = NullPrototypeObject; 6 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | module.exports = /* @__PURE__ */ (()=>{const e=function(){};return e.prototype=Object.create(null),Object.freeze(e.prototype),e})() 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | audit=false 2 | fund=false 3 | loglevel=error 4 | package-lock=false 5 | prefer-dedupe=true 6 | prefer-offline=false 7 | resolution-mode=highest 8 | save-prefix=~ 9 | save=false 10 | shamefully-hoist=true 11 | strict-peer-dependencies=false 12 | unsafe-perm=true 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: '/' 5 | schedule: 6 | interval: daily 7 | - package-ecosystem: 'github-actions' 8 | directory: '/' 9 | schedule: 10 | # Check for updates to GitHub Actions every weekday 11 | interval: 'daily' 12 | -------------------------------------------------------------------------------- /benchmark/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@null-prototype-object/benchmark", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "Kiko Beats", 11 | "license": "ISC", 12 | "packageManager": "pnpm@10.9.0" 13 | } 14 | -------------------------------------------------------------------------------- /benchmark/README.md: -------------------------------------------------------------------------------- 1 | # Benchmark 2 | 3 | ``` 4 | NullProtoObj via constructor x 207,586,282 ops/sec ±4.80% (81 runs sampled) 5 | Object.create(null) x 54,415,324 ops/sec ±2.01% (89 runs sampled) 6 | {} (normal object) x 194,340,713 ops/sec ±5.15% (77 runs sampled) 7 | {__proto__:null} x 39,313,923 ops/sec ±2.37% (92 runs sampled) 8 | 9 | Fastest is NullProtoObj via constructor 10 | ``` 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | max_line_length = 80 13 | indent_brace_style = 1TBS 14 | spaces_around_operators = true 15 | quote_type = auto 16 | 17 | [package.json] 18 | indent_style = space 19 | indent_size = 2 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ############################ 2 | # npm 3 | ############################ 4 | node_modules 5 | npm-debug.log 6 | .node_history 7 | yarn.lock 8 | package-lock.json 9 | 10 | ############################ 11 | # tmp, editor & OS files 12 | ############################ 13 | .tmp 14 | *.swo 15 | *.swp 16 | *.swn 17 | *.swm 18 | .DS_Store 19 | *# 20 | *~ 21 | .idea 22 | *sublime* 23 | nbproject 24 | 25 | ############################ 26 | # Tests 27 | ############################ 28 | testApp 29 | coverage 30 | .nyc_output 31 | 32 | ############################ 33 | # Other 34 | ############################ 35 | .env 36 | .envrc 37 | -------------------------------------------------------------------------------- /benchmark/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Benchmark = require('benchmark') 4 | 5 | const NullProtoObj = require('..') 6 | 7 | // Benchmark suite 8 | const suite = new Benchmark.Suite() 9 | 10 | suite 11 | .add('NullProtoObj via constructor', function () { 12 | const obj = new NullProtoObj() 13 | obj.foo = 123 14 | }) 15 | .add('Object.create(null)', function () { 16 | const obj = Object.create(null) 17 | obj.foo = 123 18 | }) 19 | .add('{} (normal object)', function () { 20 | const obj = {} 21 | obj.foo = 123 22 | }) 23 | .add('{__proto__:null}', function () { 24 | const obj = { __proto__: null } 25 | obj.foo = 123 26 | }) 27 | .on('cycle', function (event) { 28 | console.log(String(event.target)) 29 | }) 30 | .on('complete', function () { 31 | console.log('Fastest is ' + this.filter('fastest').map('name')) 32 | }) 33 | .run({ async: false }) 34 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: pull_request 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | test: 13 | if: github.ref != 'refs/heads/master' 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v6 18 | with: 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | - name: Setup Node.js 21 | uses: actions/setup-node@v6 22 | with: 23 | node-version: lts/* 24 | - name: Setup PNPM 25 | uses: pnpm/action-setup@v4 26 | with: 27 | version: latest 28 | run_install: true 29 | - name: Test 30 | run: pnpm test 31 | - name: Report 32 | run: npx c8 report --reporter=text-lcov > coverage/lcov.info 33 | - name: Coverage 34 | uses: coverallsapp/github-action@main 35 | with: 36 | github-token: ${{ secrets.GITHUB_TOKEN }} 37 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### 1.2.5 (2025-11-21) 6 | 7 | ### 1.2.4 (2025-10-14) 8 | 9 | ### 1.2.3 (2025-09-05) 10 | 11 | ### 1.2.2 (2025-08-13) 12 | 13 | ### 1.2.1 (2025-07-30) 14 | 15 | ## 1.2.0 (2025-05-22) 16 | 17 | 18 | ### Features 19 | 20 | * add typescript definition ([#3](https://github.com/kikobeats/null-prototype-object/issues/3)) ([76106dd](https://github.com/kikobeats/null-prototype-object/commit/76106dd02d1d1258d969738e56e53cd30a55aa20)) 21 | 22 | ## 1.1.0 (2025-05-22) 23 | 24 | 25 | ### Features 26 | 27 | * add typescript definition ([#3](https://github.com/kikobeats/null-prototype-object/issues/3)) ([76106dd](https://github.com/kikobeats/null-prototype-object/commit/76106dd02d1d1258d969738e56e53cd30a55aa20)) 28 | 29 | ### 1.0.1 (2025-05-21) 30 | 31 | ## [1.0.0](https://github.com/kikobeats/null-prototype-object/compare/v0.0.1...v1.0.0) (2025-05-19) 32 | 33 | ### 0.0.1 (2025-05-19) 34 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | 3 | const NullProtoObj = require('..') 4 | 5 | test('prototype of instance is NullProtoObj.prototype', t => { 6 | const obj = new NullProtoObj() 7 | t.is(Object.getPrototypeOf(obj), NullProtoObj.prototype) 8 | }) 9 | 10 | test('NullProtoObj.prototype has null prototype', t => { 11 | t.is(Object.getPrototypeOf(NullProtoObj.prototype), null) 12 | }) 13 | 14 | test('object does not inherit Object.prototype methods', t => { 15 | const obj = new NullProtoObj() 16 | t.is(typeof obj.toString, 'undefined') 17 | t.is(typeof obj.hasOwnProperty, 'undefined') 18 | }) 19 | 20 | test('can assign and retrieve properties', t => { 21 | const obj = new NullProtoObj() 22 | obj.foo = 'bar' 23 | t.is(obj.foo, 'bar') 24 | }) 25 | 26 | test('instances do not share state', t => { 27 | const a = new NullProtoObj() 28 | const b = new NullProtoObj() 29 | a.key = 'value' 30 | t.is(b.key, undefined) 31 | }) 32 | 33 | test('prototype is frozen', t => { 34 | const proto = Object.getPrototypeOf(new NullProtoObj()) 35 | const isFrozen = Object.isFrozen(proto) 36 | t.true(isFrozen) 37 | }) 38 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2025 Kiko Beats (kikobeats.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | contributors: 10 | if: "${{ github.event.head_commit.message != 'build: contributors' }}" 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v6 15 | with: 16 | fetch-depth: 0 17 | token: ${{ secrets.GITHUB_TOKEN }} 18 | - name: Setup Node.js 19 | uses: actions/setup-node@v6 20 | with: 21 | node-version: lts/* 22 | - name: Contributors 23 | run: | 24 | git config --global user.email ${{ secrets.GIT_EMAIL }} 25 | git config --global user.name ${{ secrets.GIT_USERNAME }} 26 | npm run contributors 27 | - name: Push changes 28 | run: | 29 | git push origin ${{ github.head_ref }} 30 | 31 | release: 32 | if: | 33 | !startsWith(github.event.head_commit.message, 'chore(release):') && 34 | !startsWith(github.event.head_commit.message, 'docs:') && 35 | !startsWith(github.event.head_commit.message, 'ci:') 36 | needs: [contributors] 37 | runs-on: ubuntu-latest 38 | steps: 39 | - name: Checkout 40 | uses: actions/checkout@v6 41 | with: 42 | token: ${{ secrets.GITHUB_TOKEN }} 43 | - name: Setup Node.js 44 | uses: actions/setup-node@v6 45 | with: 46 | node-version: lts/* 47 | - name: Setup PNPM 48 | uses: pnpm/action-setup@v4 49 | with: 50 | version: latest 51 | run_install: true 52 | - name: Test 53 | run: pnpm test 54 | - name: Report 55 | run: npx c8 report --reporter=text-lcov > coverage/lcov.info 56 | - name: Coverage 57 | uses: coverallsapp/github-action@main 58 | with: 59 | github-token: ${{ secrets.GITHUB_TOKEN }} 60 | - name: Release 61 | env: 62 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 63 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 64 | run: | 65 | git config --global user.email ${{ secrets.GIT_EMAIL }} 66 | git config --global user.name ${{ secrets.GIT_USERNAME }} 67 | git pull origin master 68 | pnpm run release 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "null-prototype-object", 3 | "description": "Fastest way for creating null-prototype objects in JavaScript", 4 | "homepage": "https://github.com/kikobeats/null-prototype-object", 5 | "version": "1.2.5", 6 | "types": "src/index.d.ts", 7 | "main": "src/index.js", 8 | "exports": { 9 | ".": { 10 | "types": "./index.d.ts", 11 | "default": "./src/index.js" 12 | } 13 | }, 14 | "author": { 15 | "email": "josefrancisco.verdu@gmail.com", 16 | "name": "Kiko Beats", 17 | "url": "https://kikobeats.com" 18 | }, 19 | "contributors": [ 20 | { 21 | "name": "Sukka", 22 | "email": "github@skk.moe" 23 | } 24 | ], 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/kikobeats/null-prototype-object.git" 28 | }, 29 | "bugs": { 30 | "url": "https://github.com/kikobeats/null-prototype-object/issues" 31 | }, 32 | "keywords": [ 33 | "dictionary", 34 | "fast", 35 | "no-prototype", 36 | "null-prototype", 37 | "object", 38 | "prototype", 39 | "secure", 40 | "utility" 41 | ], 42 | "devDependencies": { 43 | "@commitlint/cli": "latest", 44 | "@commitlint/config-conventional": "latest", 45 | "@ksmithut/prettier-standard": "latest", 46 | "ava": "latest", 47 | "c8": "latest", 48 | "ci-publish": "latest", 49 | "finepack": "latest", 50 | "git-authors-cli": "latest", 51 | "github-generate-release": "latest", 52 | "nano-staged": "latest", 53 | "simple-git-hooks": "latest", 54 | "standard": "latest", 55 | "standard-markdown": "latest", 56 | "standard-version": "latest" 57 | }, 58 | "engines": { 59 | "node": ">= 20" 60 | }, 61 | "files": [ 62 | "src" 63 | ], 64 | "scripts": { 65 | "clean": "rm -rf node_modules", 66 | "contributors": "(npx git-authors-cli && npx finepack && git add package.json && git commit -m 'build: contributors' --no-verify) || true", 67 | "coverage": "c8 report --reporter=text-lcov > coverage/lcov.info", 68 | "lint": "standard", 69 | "postrelease": "npm run release:tags && npm run release:github && (ci-publish || npm publish --access=public)", 70 | "pretest": "npm run lint", 71 | "release": "standard-version -a", 72 | "release:github": "github-generate-release", 73 | "release:tags": "git push --follow-tags origin HEAD:master", 74 | "test": "c8 ava" 75 | }, 76 | "license": "MIT", 77 | "commitlint": { 78 | "extends": [ 79 | "@commitlint/config-conventional" 80 | ], 81 | "rules": { 82 | "body-max-line-length": [ 83 | 0 84 | ] 85 | } 86 | }, 87 | "nano-staged": { 88 | "*.js": [ 89 | "prettier-standard", 90 | "standard --fix" 91 | ], 92 | "*.md": [ 93 | "standard-markdown" 94 | ], 95 | "package.json": [ 96 | "finepack" 97 | ] 98 | }, 99 | "simple-git-hooks": { 100 | "commit-msg": "npx commitlint --edit", 101 | "pre-commit": "npx nano-staged" 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # null-prototype-object 2 | 3 | > A minimal utility for creating **objects with a `null` prototype** using a **reusable constructor**. 4 | 5 | ![Last version](https://img.shields.io/github/tag/Kikobeats/null-prototype-object.svg?style=flat-square) 6 | [![Coverage Status](https://img.shields.io/coveralls/Kikobeats/null-prototype-object.svg?style=flat-square)](https://coveralls.io/github/Kikobeats/null-prototype-object) 7 | [![NPM Status](https://img.shields.io/npm/dm/null-prototype-object.svg?style=flat-square)](https://www.npmjs.org/package/null-prototype-object) 8 | 9 | ## Why not just `Object.create(null)` 10 | 11 | `Object.create(null)` gives you a clean object with no prototype — useful for: 12 | 13 | - Safe key-value maps 14 | - Avoiding inherited methods (`toString`, `hasOwnProperty`, etc.) 15 | - Preventing prototype pollution 16 | 17 | But there's a performance cost in high-frequency scenarios. 18 | 19 | ### The problem 20 | 21 | Each call to `Object.create(null)` creates a **new object shape** (hidden class). 22 | 23 | JavaScript engines like V8 can't optimize repeated use because: 24 | 25 | - The prototype isn't shared 26 | - Shapes can't be reused 27 | - Inline caching and JIT optimizations break down 28 | - It leads to **megamorphic** call sites (a de-optimization trigger) 29 | 30 | ### The solution: `null-prototype-object` 31 | 32 | This package provides a constructor with a **frozen, shared null-prototype**, enabling V8 to: 33 | 34 | - Reuse a stable hidden class 35 | - Inline property access 36 | - Optimize memory layout 37 | - Avoid dynamic shape transitions 38 | 39 | ### Why it’s faster 40 | 41 | | Feature | `Object.create(null)` | `new NullProtoObj()` | 42 | |---------------------------|------------------------|------------------------| 43 | | Shared prototype | ❌ | ✅ | 44 | | Hidden class reuse | ❌ | ✅ | 45 | | Inline caching | ❌ | ✅ | 46 | | JIT-friendly | ❌ | ✅ | 47 | | Memory efficient | ❌ | ✅ | 48 | 49 | ### When to use it 50 | 51 | Use `null-prototype-object` if: 52 | 53 | - You're allocating many null-prototype objects (e.g. parsers, serializers, caches). 54 | - You want predictable performance in tight loops. 55 | - You're optimizing object creation in hot code paths. 56 | 57 | ## Install 58 | 59 | ```bash 60 | $ npm install null-prototype-object --save 61 | ``` 62 | 63 | ## Usage 64 | 65 | ```js 66 | const NullProtoObj = require('null-prototype-object') 67 | 68 | const obj = new NullProtoObj() 69 | 70 | // No inherited methods 71 | console.log(obj.toString) // undefined 72 | 73 | // Safe for dictionary-style use 74 | obj.__proto__ = 'polluted? nope' 75 | console.log(obj.__proto__) // => "polluted? nope" 76 | 77 | console.log(obj.foo) 78 | obj.foo = 'bar' 79 | console.log(Object.getPrototypeOf(obj)) // ==> null (via prototype chain) 80 | ``` 81 | 82 | ## Benchmark 83 | 84 | ``` 85 | NullProtoObj via constructor x 207,586,282 ops/sec ±4.80% (81 runs sampled) 86 | Object.create(null) x 54,415,324 ops/sec ±2.01% (89 runs sampled) 87 | {} (normal object) x 194,340,713 ops/sec ±5.15% (77 runs sampled) 88 | {__proto__:null} x 39,313,923 ops/sec ±2.37% (92 runs sampled) 89 | 90 | Fastest is NullProtoObj via constructor 91 | ``` 92 | 93 | ## License 94 | 95 | **null-prototype-object** © [Kiko Beats](https://kikobeats.com), released under the [MIT](https://github.com/kikobeats/null-prototype-object/blob/master/LICENSE.md) License. 96 | 97 | Credits to [pi0](https://github.com/h3js/rou3/blame/main/src/_utils.ts) and [anonrig](https://github.com/anonrig/fast-querystring/blame/main/lib/parse.js#L6). Maintained by [Kiko Beats](https://kikobeats.com) with help from [contributors](https://github.com/kikobeats/null-prototype-object/contributors). 98 | 99 | > [kikobeats.com](https://kikobeats.com) · GitHub [Kiko Beats](https://github.com/kikobeats) · Twitter [@kikobeats](https://twitter.com/kikobeats) 100 | --------------------------------------------------------------------------------