阅读视图

发现新文章,点击刷新页面。

JavaScript原型链 - 继承的基石与核心机制

前言:从一道面试题说起

function Person() {}
const person = new Person();

console.log(person.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null

console.log(person.constructor === Person); // true
console.log(Person.constructor === Function); // true
console.log(person.constructor === Person.prototype.constructor); // true (!)
console.log(Function.constructor === Function); // true (!)

如果你能完全理解上面的代码,那么你已经掌握了原型链的核心。如果不能,本篇文章将带我们一步步揭开原型链的神秘面纱。

构造函数、实例、原型的三者关系

在JavaScript中,每个对象都有一个特殊的内部属性[[Prototype]](可以通过__proto__访问),它指向该对象的原型对象。

function Person(name) {
    this.name = name;
}

// 原型对象:所有实例共享的方法和属性
Person.prototype.sayHello = function() {
    console.log(`Hello, I'm ${this.name}`);
};

// 实例:通过new创建的对象
const zhangsan = new Person('zhangsna');

// 三者关系验证
console.log(zhangsan.__proto__ === Person.prototype); // true
console.log(Person.prototype.constructor === Person); // true

prototype和__proto__的区别与联系

  • prototype 是函数特有的属性。
  • __proto__ 是每个对象都有的属性。
function Foo() {}
const obj = new Foo();

console.log(typeof Foo.prototype); // "object"
console.log(typeof obj.__proto__); // "object"

console.log(Foo.prototype === obj.__proto__); // true
console.log(Foo.__proto__ === Function.prototype); // true

// 一定要注意:函数也是对象,所以函数也有__proto__
console.log(Foo.__proto__ === Function.prototype); // true
console.log(Function.prototype.__proto__ === Object.prototype); // true
  1. 每个函数都有一个 prototype 属性,指向该函数的原型对象
  2. 每个对象都有一个 __proto__ 属性,指向创建该对象的构造函数的原型对象
  3. 原型对象的 constructor 属性指向创建该实例对象的构造函数。
  4. Function 是一个特殊的函数,它的 constructor 指向它自己。

完整的原型链结构

function Animal(name) {
    this.name = name;
}

Animal.prototype.eat = function() {
    console.log(`${this.name} is eating`);
};

function Dog(name, breed) {
    Animal.call(this, name); // 调用父类构造函数
    this.breed = breed;
}

// 设置原型链
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // 修复constructor指向

Dog.prototype.bark = function() {
    console.log(`${this.name} is barking`);
};

const myDog = new Dog('Buddy', 'Golden Retriever');

// 原型链查找路径:
console.log(myDog.__proto__ === Dog.prototype); // true
console.log(Dog.prototype.__proto__ === Animal.prototype); // true
console.log(Animal.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null

上述代码中,原型链的查找过程:

  1. myDog 本身有 namebreed 属性
  2. myDog.__proto__ (Dog.prototype) 有 bark 方法
  3. Dog.prototype.__proto__ (Animal.prototype) 有 eat 方法
  4. Animal.prototype.__proto__ (Object.prototype) 有 toString 等方法
  5. Object.prototype.__proto__null,查找结束

Object.prototype是所有原型链的终点。

属性屏蔽规则

hasOwnProperty vs in操作符

  • hasOwnProperty: 检查属性是否在对象自身(不在原型链上)
  • in操作符: 检查属性是否在对象自身或原型链上

属性屏蔽的三种情况

function Parent() {
    this.value = 'parent value';
}

Parent.prototype.shared = 'parent shared';

function Child() {
    this.value = 'child value'; 
}

Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

Child.prototype.shared = 'child shared'; 

const child = new Child();

情况1:对象自身有该属性(完全屏蔽)

以上述代码为例,Child 本身有自己的 value 属性,当调用 value 属性时,会完全屏蔽原型链上的同名 value 属性:

console.log(child.value); // 'child value'(不是'parent value')
console.log(child.hasOwnProperty('value')); // true

情况2:对象自身本来没有,但添加对象自身属性

console.log(child.shared); // 'child shared'(来自Child.prototype)
console.log(child.hasOwnProperty('shared')); // false

// 添加对象自身属性
child.shared = 'instance shared';
console.log(child.shared); // 'instance shared'(现在自身有了)
console.log(child.hasOwnProperty('shared')); // true

情况3:属性是只读的

Parent.prototype.readOnly = 'cannot change';

// 试图修改只读属性
child.readOnly = 'try to change';
console.log(child.readOnly); // 'cannot change'(修改失败)
console.log(child.hasOwnProperty('readOnly')); // false(没有添加成功)

原型链的基础结构

function Person(name) {
    this.name = name;
}

Person.prototype.sayHello = function() {
    console.log(`Hello, ${this.name}`);
};

const zhangsan = new Person('zhangsan');

其原型结构图如下:

zhangsan (实例)
  ├── __proto__: Person.prototype
  │      ├── constructor: Person
  │      ├── sayHello: function()
  │      └── __proto__: Object.prototype
  │             ├── constructor: Object
  │             ├── toString: function()
  │             ├── hasOwnProperty: function()
  │             └── __proto__: null
  ├── name: "zhangsan"
  └── (其他实例属性...)

Person (构造函数)
  ├── prototype: Person.prototype
  └── __proto__: Function.prototype
           ├── constructor: Function
           ├── apply: function()
           ├── call: function()
           └── __proto__: Object.prototype

原型链的实际应用

实现继承的多种方式

原型链继承(经典方式)

function Parent(name) {
    this.name = name;
}

function Child(age) {
    this.age = age;
}

// 关键:让子类原型指向父类实例
Child.prototype = new Parent();

// 修复constructor指向
Child.prototype.constructor = Child;
  • 问题:引用类型的属性会被所有实例共享

组合继承(最常用)

function Parent(name) {
    this.name = name ;
}

function Child(name, age) {
    // 继承属性
    Parent.call(this, name);  // 第二次调用Parent
    this.age = age;
}

// 继承方法
Child.prototype = new Parent();  // 第一次调用Parent
Child.prototype.constructor = Child;
  • 优点:结合了原型链和构造函数的优点
  • 缺点:父类构造函数被调用了两次

寄生组合式继承(最佳实践)

function inheritPrototype(child, parent) {
    // 创建父类原型的副本
    const prototype = Object.create(parent.prototype);
    // 修复constructor指向
    prototype.constructor = child;
    // 设置子类原型
    child.prototype = prototype;
}

function Parent(name) {
    this.name = name;
}

function Child(name, age) {
    Parent2.call(this, name);
    this.age = age;
}

inheritPrototype(Child, Parent);
  • 只调用一次父类构造函数
  • 避免在子类原型上创建不必要的属性
  • 原型链保持不变

ES6 class继承

class Parent3 {
    constructor(name) {
        this.name = name;
    }
}

class Child3 extends Parent3 {
    constructor(name, age) {
        super(name);
        this.age = age;
    }
    
    sayAge() {
        console.log(this.age);
    }
}
  • 语法简洁,现代解决方案,但需要ES6+支持

实现混入(Mixin)

const canEat = {
    eat: function(food) {
        console.log(`${this.name} is eating ${food}`);
        this.energy += 10;
    }
};

const canSleep = {
    sleep: function(hours) {
        console.log(`${this.name} is sleeping for ${hours} hours`);
        this.energy += hours * 5;
    }
};

const canWalk = {
    walk: function(distance) {
        console.log(`${this.name} is walking ${distance} km`);
        this.energy -= distance * 2;
    }
};

// 混入函数
function mixin(target, ...sources) {
    Object.assign(target.prototype, ...sources);
}

// 创建动物类
function Animal(name) {
    this.name = name;
    this.energy = 100;
}

mixin(Animal, canEat, canSleep, canWalk);

// 创建鸟类,额外添加飞行能力
const canFly = {
    fly: function(distance) {
        console.log(`${this.name} is flying ${distance} km`);
        this.energy -= distance * 5;
    }
};

function Bird(name) {
    Animal.call(this, name);
    this.wings = 2;
}

// 设置原型链
Bird.prototype = Object.create(Animal.prototype);
Bird.prototype.constructor = Bird;

// 添加飞行能力
Object.assign(Bird.prototype, canFly);

原型链常见陷阱

陷阱1:修改原型会影响所有实例

function Person(name) {
    this.name = name;
}

const zhangsan = new Person('zhangsan');
const lisi = new Person('lisi');

// 修改原型
Person.prototype.sayHello = function() {
    console.log(`Hello, ${this.name}`);
};

zhangsan.sayHello(); // Hello, zhangsan (正常)
lisi.sayHello();   // Hello, lisi (正常)

陷阱2:原型上的引用类型属性被所有实例共享

function Problem() {
    this.values = []; // 正确:每个实例有自己的数组
}

Problem.prototype.sharedValues = []; // 错误:所有实例共享同一个数组

const p1 = new Problem();
const p2 = new Problem();

p1.sharedValues.push('from p1');
p2.sharedValues.push('from p2');

console.log(p1.sharedValues); // ['from p1', 'from p2']
console.log(p2.sharedValues); // ['from p1', 'from p2']

陷阱3:for...in会遍历原型链上的可枚举属性

function Parent() {
    this.parentProp = 'parent';
}

Parent.prototype.inheritedProp = 'inherited';

function Child() {
    this.childProp = 'child';
}

Child.prototype = new Parent();

const child = new Child();

for (let key in child) {
    console.log(key); // childProp, parentProp, inheritedProp
}

解决方案:使用hasOwnProperty过滤:

for (let key in child) {
    if (child.hasOwnProperty(key)) {
        console.log(key); // childProp, parentProp
    }
}

原型链的最佳实践

实践1:使用Object.create设置原型链

function Parent(name) {
    this.name = name;
}

Parent.prototype.sayName = function() {
    console.log(this.name);
};

function Child(name, age) {
    Parent.call(this, name);
    this.age = age;
}

// 最佳方式
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

// 添加子类方法
Child.prototype.sayAge = function() {
    console.log(this.age);
};

实践2:使用class语法(ES6+)

class GoodParent {
    constructor(name) {
        this.name = name;
    }
    
    sayName() {
        console.log(this.name);
    }
}

class GoodChild extends GoodParent {
    constructor(name, age) {
        super(name);
        this.age = age;
    }
    
    sayAge() {
        console.log(this.age);
    }
}

实践3:安全地检查属性

const obj = { ownProp: 'value' };

// 不好的做法
if (obj.property) {
    // 如果property值为falsy(0, '', false, null, undefined),会被误判
}

// 好的做法
if (obj.hasOwnProperty('property')) {
    // 明确检查自身属性
}

// 更好的做法(防止hasOwnProperty被覆盖)
if (Object.prototype.hasOwnProperty.call(obj, 'property')) {
    // 最安全的方式
}

实践4:避免修改内置对象的原型

// 非必要情况,不得进行以下操作
if (!Array.prototype.customMethod) {
    Array.prototype.customMethod = function() {
        // 实现
    };
}

思考题:以下代码的输出是什么?为什么?

function Foo() {}
function Bar() {}

Bar.prototype = Object.create(Foo.prototype);

const bar = new Bar();

console.log(bar instanceof Bar); 
console.log(bar instanceof Foo); 
console.log(bar instanceof Object);

console.log(Bar.prototype.isPrototypeOf(bar)); 
console.log(Foo.prototype.isPrototypeOf(bar)); 
console.log(Object.prototype.isPrototypeOf(bar)); 

结语

原型链是 JavaScript 面向对象编程的基石,在 JavaScript 中没有真正的类,只有对象和它们之间的链接(原型链),对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!

浏览器缓存:从底层原理到最佳实践

缓存是前端性能优化的核心,但我们真的理解各种缓存之间的关系吗?本篇文章将彻底解析浏览器缓存机制,帮我们建立完整的知识体系。

缓存全景图:一张图看懂所有缓存关系

缓存全景图

HTTP缓存:强缓存 vs 协商缓存

HTTP缓存是浏览器缓存体系的核心,分为两大策略:强缓存协商缓存

强缓存:浏览器自主决策

浏览器检查本地缓存是否在有效期内,若有效则直接使用,不发送任何网络请求。

# 第一次请求:服务器设置缓存策略
GET /static/logo.png
HTTP/1.1 200 OK
Cache-Control: max-age=31536000, public
Expires: Mon, 01 Jan 2024 00:00:00 GMT

# 后续请求:浏览器自主决策(在有效期内)
GET /static/logo.png
# 浏览器检查:max-age=31536000 → 还有效!
# 直接从缓存返回,不发送网络请求

协商缓存:服务端验证

当请求资源时,浏览器携带缓存标识询问服务器资源是否变更,若没有变更,直接从缓存中读取资源。

# 第一次请求:服务器返回资源标识
GET /api/user/profile
HTTP/1.1 200 OK
ETag: "abc123xyz"
Last-Modified: Wed, 21 Oct 2022 07:28:00 GMT
Cache-Control: no-cache

# 第二次请求:带上标识询问服务器
GET /api/user/profile
If-None-Match: "abc123xyz"
If-Modified-Since: Wed, 21 Oct 2022 07:28:00 GMT

# 服务器响应
HTTP/1.1 304 Not Modified
# 资源未变,使用本地缓存

强缓存 vs 协商缓存对比:

特性 强缓存 协商缓存
决策机制 浏览器自己决定 浏览器和服务器协商
检查时机 优先检查 强缓存失效后检查
网络请求 可能完全不发请求 一定发送请求(验证)
典型状态码 200 (from cache) 304 (Not Modified)
响应速度 极快(直接本地读取) 较慢(需要网络往返)
适用场景 版本固定的静态资源 频繁更新的动态资源
更新时机 缓存过期时更新 每次请求都验证

浏览器缓存:Memory Cache vs Disk Cache

Memory Cache:高速内存缓存

  • 位置:浏览器进程内存空间
  • 容量:有限,依赖设备内存
  • 生命周期:会话级别,标签页关闭则释放
  • 特点:读取速度极快(纳秒级)

适用场景

  • 当前页面已加载的资源
  • 预加载的脚本和样式
  • 小体积的Base64图片

Disk Cache:持久化磁盘缓存

  • 位置:用户磁盘的缓存目录
  • 容量:较大(通常数百MB到数GB)
  • 生命周期:长期持久化
  • 特点:读取速度较慢(毫秒级),但容量大

适用场景

  • 大体积静态资源(图片、字体、视频)
  • 跨会话共享的资源
  • 低频访问但需要持久化的内容

Service Worker:应用层缓存控制

Service Worker 运行在浏览器后台,作为代理服务器,赋予开发者完全控制缓存的能力。

Service Worker 缓存特点

  1. 独立线程运行,不阻塞主线程
  2. 完全控制网络请求,可自定义缓存策略
  3. 支持离线体验,是PWA的核心技术
  4. 生命周期独立,可长期缓存资源

Service Worker 缓存策略

Service Worker 使用的是典型的缓存优先策略:

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(cachedResponse => {
        // 1. 缓存优先
        if (cachedResponse) {
          // 后台更新缓存
          event.waitUntil(
            updateCache(event.request)
          );
          return cachedResponse;
        }
        
        // 2. 网络回退
        return fetch(event.request)
          .then(networkResponse => {
            // 缓存新资源
            return cacheResponse(event.request, networkResponse);
          })
          .catch(() => {
            // 3. 回退到离线页面
            return caches.match('/offline.html');
          });
      })
  );
});

Service Worker 使用场景

场景 策略 优势
离线应用 Cache-First + Network-Fallback 提供完整的离线体验
性能优化 Stale-While-Revalidate 快速响应+后台更新
资源预加载 Precache + Runtime Caching 减少关键资源加载时间
不可靠网络 Network-First + Cache-Fallback 提升弱网环境体验

缓存查找顺序

async function fetchWithCache(request) {
  // 1. Service Worker 拦截(最高优先级)
  if (navigator.serviceWorker?.controller) {
    const swResponse = await checkServiceWorkerCache(request);
    if (swResponse) return swResponse;
  }
  
  // 2. Memory Cache 检查(最快路径)
  const memoryCached = memoryCache.get(request.url);
  if (memoryCached && !isExpired(memoryCached)) {
    reportCacheHit('memory');
    return memoryCached;
  }
  
  // 3. Disk Cache 检查(强缓存验证)
  const diskCached = await checkDiskCache(request);
  if (diskCached) {
    if (diskCached.cacheControl === 'immutable' || 
        !isExpired(diskCached)) {
      // 更新 Memory Cache 提升后续访问速度
      memoryCache.set(request.url, diskCached);
      reportCacheHit('disk');
      return diskCached;
    }
  }
  
  // 4. 准备网络请求(设置协商缓存头)
  const init = prepareRequest(request);
  
  // 5. 发起网络请求
  let response = await fetch(request, init);
  
  // 6. 处理响应,决定是否缓存
  if (shouldCache(response)) {
    await cacheInLayers(request, response.clone());
  }
  
  return response;
}

  • 强缓存资源:可以同时存在于Memory和Disk Cache,浏览器根据大小和类型智能分配
  • 协商缓存资源:主要在Disk Cache,Memory Cache可能不存储或短期存储
  • 开发者控制策略:通过HTTP头控制缓存策略,浏览器自动选择存储位置
  • 性能考量:小文件强缓存 → Memory Cache极速;大文件强缓存 → Disk Cache持久

本地存储体系

存储类型 容量 生命周期 同步性 使用场景
Cookie 4KB 可设置过期时间 每次请求自动携带 用户认证、服务端状态
LocalStorage 5-10MB 永久(除非手动清除) 同步阻塞 用户偏好、应用配置
SessionStorage 5-10MB 标签页关闭时清除 同步阻塞 表单草稿、临时状态
IndexedDB 数百MB 永久 异步非阻塞 大量结构化数据
Cache API 依赖设备 Service Worker控制 异步 网络资源缓存、PWA

Chrome DevTools中的缓存标识

在Chrome开发者工具中,不同缓存来源有不同的标识:

Size列显示 状态码 含义 缓存来源
(memory cache) 200 内存缓存 Memory Cache
(disk cache) 200 磁盘缓存 Disk Cache
(ServiceWorker) 200 Service Worker缓存 Service Worker Cache
文件大小 304 协商缓存生效 服务器验证通过
文件大小 200 新请求 网络获取

现代框架下的缓存最佳实践

Vue 3 + Vite 缓存配置

vite.config.js 中配置:

export default defineConfig({
  build: {
    // 生成带hash的文件名,实现长期缓存
    rollupOptions: {
      output: {
        entryFileNames: 'assets/[name]-[hash].js',
        chunkFileNames: 'assets/[name]-[hash].js',
        assetFileNames: 'assets/[name]-[hash].[ext]'
      }
    }
  },
  
  server: {
    headers: {
      // 开发环境:禁用缓存
      'Cache-Control': 'no-store'
    }
  }
});

生产环境 Nginx 配置:

server {
    # HTML文件:短缓存 + 协商缓存
    location = /index.html {
        add_header Cache-Control "public, max-age=300, must-revalidate";
    }
    
    # 带hash的静态资源:长期缓存 + 不可变
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2)$ {
        # 匹配带hash的文件名
        if ($request_uri ~* "\.([0-9a-f]{8,})\.(js|css|png|jpg|jpeg|gif|ico|svg|woff2)$") {
            add_header Cache-Control "public, max-age=31536000, immutable";
        }
        # 无hash的资源使用协商缓存
        if ($request_uri !~* "\.([0-9a-f]{8,})\.(js|css|png|jpg|jpeg|gif|ico|svg|woff2)$") {
            add_header Cache-Control "public, max-age=604800, must-revalidate";
        }
    }
    
    # API请求:不缓存或短缓存
    location /api/ {
        add_header Cache-Control "no-store, no-cache, must-revalidate";
        proxy_pass http://api-backend;
    }
}

React 缓存策略建议

使用 React Query 进行数据缓存管理:

import { useQuery, QueryClient } from '@tanstack/react-query';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000, // 5分钟
      cacheTime: 10 * 60 * 1000, // 10分钟
      retry: 1,
    },
  },
});

function UserProfile({ userId }) {
  const { data, isLoading } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
    // 根据用户交互决定缓存策略
    cacheTime: userId ? 15 * 60 * 1000 : 0, // 登录用户缓存15分钟
  });
  
  // 预加载相关数据
  const prefetchPosts = () => {
    queryClient.prefetchQuery({
      queryKey: ['posts', userId],
      queryFn: () => fetchUserPosts(userId),
    });
  };
}

常见问题与解决方案

用户反映看到的是旧版本页面

原因:HTML文件被缓存,导致加载的是旧版本的静态资源引用。 解决方案:在nginx中配置合理的缓存策略:

location / {
    # HTML文件:短时间缓存 + 协商缓存
    add_header Cache-Control "public, max-age=300, must-revalidate";
}

location /assets/ {
    # 静态资源:长时间缓存 + 文件hash
    add_header Cache-Control "public, max-age=31536000, immutable";
}

如何优雅地清除缓存?

vite/webpack 会自动生成文件hash,以此进行清除。

缓存最佳实践总结

资源类型 推荐策略 缓存时间 更新机制
HTML入口文件 协商缓存 短时间(5分钟) 内容hash或版本号
JS/CSS(带hash) 强缓存 长期(1年) 文件名hash变更
图片/字体 强缓存 中期(1个月) URL变更或版本控制
API数据(列表) 内存缓存 短期(1-5分钟) 时间过期或主动失效
用户偏好配置 LocalStorage 永久 用户操作触发更新
离线资源 Service Worker 长期 版本化预缓存

未来趋势

  • HTTP/3的缓存改进:更智能的缓存协商机制
  • AI驱动的缓存策略:根据用户行为预测缓存需求
  • 边缘计算缓存:CDN与浏览器缓存的深度融合
  • 隐私保护的缓存:沙盒化缓存防止指纹追踪

结语

浏览器缓存是一个多层次的复杂系统,理解各层缓存的工作原理、生命周期和适用场景是进行有效缓存管理的关键,对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!

JavaScript this全攻略(下)- this丢失问题与解决方案

为什么回调函数中的 this 常常"不听话"?为什么我们明明在对象内部调用方法,this 却指向了别处?本篇文章将彻底解决 this 绑定丢失的问题。

前言:this"叛变"

const user = {
  name: 'zhangsan',
  logName: function() {
    console.log(this.name);
  }
};

// 正常调用
user.logName(); // zhangsan

// this"叛变"了!
setTimeout(user.logName, 0); // undefined (或全局的name)

这是前端开发中最常见的陷阱之一。理解为什么 this 会丢失,以及如何正确固定它,是成为 JavaScript 高手的必经之路。

this绑定丢失的常见场景

场景1:回调函数中的this丢失

示例1:事件监听器

const buttonHandler = {
  clicks: 0,
  handleClick: function() {
    this.clicks++;
    console.log(`Clicked ${this.clicks} times`);
  }
};

// 错误做法
document.getElementById('myButton')
  .addEventListener('click', buttonHandler.handleClick);
// 点击时:Clicked NaN times(this指向按钮元素,而不是buttonHandler对象)

示例2:定时器回调

const timer = {
  count: 0,
  start: function() {
    setInterval(function() {
      this.count++; // this指向全局对象
      console.log(this.count); // NaN
    }, 1000);
  }
};

示例3:数组方法回调

const processor = {
  data: [1, 2, 3],
  multiplier: 10,
  process: function() {
    return this.data.map(function(item) {
      // 这里的this不是processor
      return item * this.multiplier; // NaN
    });
  }
};

console.log(processor.process()); // [NaN, NaN, NaN]

场景2:函数赋值导致的丢失

const obj = {
  value: 42,
  getValue: function() {
    return this.value;
  }
};

// 直接调用
console.log(obj.getValue()); // 42

// 赋值给变量后调用
const getValue = obj.getValue;
console.log(getValue()); // undefined

// 作为参数传递
function callCallback(callback) {
  return callback();
}

console.log(callCallback(obj.getValue)); // undefined

场景3:嵌套函数中的this丢失

const game = {
  score: 0,
  start: function() {
    console.log('Game started. Score:', this.score);
    function updateScore() {
      // 嵌套函数有自己的this绑定
      this.score += 10; // this指向全局
      console.log('Score updated:', this.score);
    }
    updateScore();
  }
};

game.start(); 
// Game started. Score: 0
// Score updated: NaN

场景4:间接引用导致的丢失

const obj1 = {
  name: 'obj1',
  getName: function() {
    return this.name;
  }
};

const obj2 = {
  name: 'obj2'
};

// 间接引用
obj2.getName = obj1.getName;

console.log(obj1.getName()); // obj1
console.log(obj2.getName()); // obj2
console.log((obj2.getName = obj1.getName)()); // undefined

场景5:严格模式的影响

function test() {
  console.log(this);
}

// 非严格模式
test(); // Window

// 严格模式
function strictTest() {
  'use strict';
  console.log(this);
}

strictTest(); // undefined

解决方案对比:三种固定this的方法

方案1:闭包保存this(传统方法)

// 使用self/that/_this保存外层this
const controller = {
  data: [],
  init: function() {
    const self = this; // 保存this
    
    document.addEventListener('click', function() {
      // 使用保存的self
      self.handleClick();
    });
    
    // 在嵌套函数中
    function helper() {
      console.log(self.data);
    }
    helper();
  },
  handleClick: function() {
    console.log('Clicked with data:', this.data);
  }
};
  • 该方案的优点:兼容性好,易于理解
  • 该方案的缺点:需要额外变量,代码略显冗余

方案2:bind方法(永久绑定this)

const logger = {
  prefix: 'LOG:',
  log: function(message) {
    console.log(this.prefix, message);
  }
};

// 绑定this
const boundLog = logger.log.bind(logger);
boundLog('Hello'); // LOG: Hello

方案3:箭头函数(继承外层this)

const counter = {
  count: 0,
  start: function() {
    // 箭头函数使用外层函数的this
    setInterval(() => {
      this.count++;
      console.log('Current count:', this.count);
    }, 1000);
  }
};

三种方案对比分析

方案 语法简洁性 性能表现 兼容性 适用场景
闭包保存this 一般 良好 优秀 简单场景,兼容性要求高
bind方法 良好 一般 良好 需要参数预设,一次绑定多次使用
箭头函数 优秀 良好 一般 ES6+环境,回调函数

特殊场景解决方案

场景1:需要动态this

有时候我们需要动态绑定 this,这时候可以使用 call()/apply() 函数动态绑定:

const contextA = { name: 'A', value: 1 };
const contextB = { name: 'B', value: 2 };

function operation() {
  return `${this.name}: ${this.value * 2}`;
}

// 动态绑定
const results = [contextA, contextB].map(context => 
  operation.call(context)
);
console.log(results); // ['A: 2', 'B: 4']

场景2:需要多个this上下文

使用遍历:批量绑定和执行

const contexts = [
  { id: 1, name: 'First' },
  { id: 2, name: 'Second' },
  { id: 3, name: 'Third' }
];

function logContext() {
  console.log(`ID: ${this.id}, Name: ${this.name}`);
}

// 批量绑定和执行
contexts.forEach(context => {
  const boundLog = logContext.bind(context);
  boundLog();
});

使用函数柯里化处理多个上下文

function createLogger(prefix) {
  return function() {
    console.log(`[${prefix}]`, this);
  };
}

const loggers = contexts.map(context => 
  createLogger(context.name).bind(context)
);

loggers[0](); 
loggers[1](); 
loggers[2](); 

最佳实践指南

实践1:推荐使用箭头函数

// 箭头函数
class Component {
  state = { count: 0 };
  // 使用箭头函数自动绑定
  handleClick = () => {
    this.setState({ count: this.state.count + 1 });
  };
}

实践2:使用bind绑定

function Library() {
  if (!(this instanceof Library)) {
    return new Library();
  }
  this.value = 0;
  this.increment = this.increment.bind(this);
}

Library.prototype.increment = function() {
  this.value++;
  return this;
};

实践3:错误处理

防御性编程与 try/catch 错误捕获:

const safeCall = function(fn, context, ...args) {
  if (typeof fn !== 'function') {
    throw new TypeError('fn must be a function');
  }
  
  // 确保context存在
  context = context || (typeof window !== 'undefined' ? window : global);
  
  try {
    return fn.apply(context, args);
  } catch (error) {
    console.error('Error in safeCall:', error);
    throw error;
  }
};

实践4:避免不必要的bind调用

class OptimizedComponent {
  constructor() {
    // 一次性绑定所有方法
    this.methods = ['handleClick', 'handleChange', 'handleSubmit']
      .reduce((obj, method) => {
        obj[method] = this[method].bind(this);
        return obj;
      }, {});
  }
  
  handleClick() { /* ... */ }
  handleChange() { /* ... */ }
  handleSubmit() { /* ... */ }
  
  // 使用预绑定的方法
  render() {
    return `
      <button onclick="${this.methods.handleClick}">Click</button>
    `;
  }
}

实践5:使用WeakMap缓存绑定结果

const bindCache = new WeakMap();

function cachedBind(fn, context) {
  if (!bindCache.has(fn)) {
    bindCache.set(fn, new WeakMap());
  }
  
  const contextMap = bindCache.get(fn);
  
  if (!contextMap.has(context)) {
    contextMap.set(context, fn.bind(context));
  }
  
  return contextMap.get(context);
}

const obj1 = { value: 1 };
const obj2 = { value: 2 };

function showValue() {
  console.log(this.value);
}

const bound1 = cachedBind(showValue, obj1);
const bound2 = cachedBind(showValue, obj1); // 从缓存获取
console.log(bound1 === bound2); // true

思考题

const obj = {
  name: 'Test',
  createHandler: function() {
    return function() {
      console.log(this.name);
    };
  }
};

const handler = obj.createHandler();
setTimeout(handler, 100);

以上代码的输出结果是什么?如何修复?欢迎在评论区分享你的答案和思考!

结语

this 绑定丢失是 JavaScript 开发中的常见问题,但通过正确的策略可以轻松解决,对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!

JavaScript this全攻略(上)- this绑定

为什么JavaScript 中的 this 的值时而指向 window,时而指向对象?为什么回调函数中的 this 会"丢失"?掌握 this 的绑定规则,让你彻底理解 JavaScript 中最令人困惑的关键字。

前言:this的困惑与魅力

const obj = {
  name: 'zhangsan',
  sayHello: function() {
    console.log(`Hello, ${this.name}`);
  }
};

obj.sayHello(); // Hello, zhangsan

const sayHello = obj.sayHello;
sayHello(); // Hello, undefined 

this 是 JavaScript 中最常用但也最容易出错的关键字之一。它不像其他语言那样固定指向当前对象,而是在运行时动态绑定,值取决于函数的调用方式。

his绑定的基本规则

JavaScript 中的 this 绑定遵循四条核心规则,优先级从低到高依次为:

  • 默认绑定(独立函数调用)
  • 隐式绑定(方法调用)
  • 显式绑定(call/apply/bind)
  • new绑定(构造函数调用)

默认绑定(独立函数调用)

当函数独立调用时,this 指向全局对象(浏览器中为 window,Node.js中为 global)。

示例1:普通函数调用

function showThis() {
  console.log(this);
}

showThis(); // 浏览器中输出:Window对象

示例2:嵌套函数调用

function outer() {
  console.log('outer this:', this); // Window
  function inner() {
    console.log('inner this:', this); // Window
  }
  inner();
}

outer();

示例3:严格模式的影响

function strictMode() {
  'use strict';
  console.log(this); // undefined
}

strictMode();

function mixMode() {
  console.log(this); // Window
  function inner() {
    'use strict';
    console.log(this); // undefined
  }
  inner();
}

mixMode();

默认绑定关键点

  • 非严格模式:this指向全局对象
  • 严格模式:this为undefined
  • 函数定义位置不影响,只关心如何调用

隐式绑定(方法调用)

当函数作为对象的方法调用时,this 指向该对象。

示例1:基本对象方法

const user = {
  name: 'zhangsan',
  greet: function() {
    console.log(`Hello, ${this.name}`);  // this指向user
  }
};

user.greet(); // Hello, zhangsan

示例2:多层嵌套对象

const company = {
  name: 'TechCorp',
  department: {
    name: 'Engineering',
    getDeptName: function() {
      console.log(this.name);
    }
  }
};

company.department.getDeptName(); // Engineering

注意:this指向直接调用者department,不是company。

示例3:动态添加方法

const car = {
  brand: 'HQ'
};

function startEngine() {
  console.log(`${this.brand} engine started`);
}

car.start = startEngine;
car.start(); // HQ engine started

示例4:方法赋值给变量

const startFunc = car.start;
startFunc(); // undefined engine started (this丢失!)

this丢失的问题,在下一篇文章中会详细讲解。

隐式绑定的核心

this指向调用函数的对象,即.前面的对象。

显式绑定(call/apply/bind)

通过call()apply()bind() 函数,可以显式指定 this 的值。

示例1:call方法 - 立即执行

function introduce(language, hobby) {
  console.log(`I'm ${this.name}, I code in ${language}, I like ${hobby}`);
}

const person1 = { name: 'zhangsan' };
const person2 = { name: 'lisi' };

introduce.call(person1, 'JavaScript', 'reading'); 
// I'm zhangsan, I code in JavaScript, I like reading

introduce.call(person2, 'Python', 'gaming');
// I'm lisi, I code in Python, I like gaming

示例2:apply方法 - 参数以数组传递

introduce.apply(person1, ['JavaScript', 'reading']);
// 效果与call相同,只是参数形式不同

示例3:bind方法 - 创建新函数,稍后执行

const introduceAlice = introduce.bind(person1);
introduceAlice('JavaScript', 'reading');  // 不会立即执行,只有在需要的时候才会执行

示例4:bind的参数预设

const introduceWithLang = introduce.bind(person1, 'JavaScript');
introduceWithLang('reading');

示例5:bind的连续绑定

const obj1 = { x: 1 };
const obj2 = { x: 2 };

function getX() {
  return this.x;
}

const boundToObj1 = getX.bind(obj1);
const boundToObj2 = getX.bind(obj2);

console.log(boundToObj1()); // 1
console.log(boundToObj2()); // 2

// bind的硬绑定特性:永久绑定this
const permanentlyBound = getX.bind(obj1);
const testObj = { x: 3, getX: permanentlyBound };
console.log(testObj.getX()); // 仍然是1,不是3!

显式绑定的特点

  • call()/apply():立即调用函数,指定this和参数。
  • bind():返回新函数,永久绑定this,可部分应用参数。

new绑定(构造函数调用)

使用 new 关键字调用构造函数时,this 指向新创建的对象实例。

示例1:基本构造函数

function Person(name, age) {
  // new调用时,this指向新创建的空对象
  this.name = name;
  this.age = age;
  
  // 不需要return,默认返回this
}
const alice = new Person('zhangsan', 25);
console.log(alice.name); // zhangsan
console.log(alice.age);  // 25

示例2:构造函数返回值

function Car(brand) {
  this.brand = brand;
  // 如果返回原始值,会被忽略,仍然返回this
  return 'test'; // 被忽略
  // 如果返回对象,则覆盖this
  // return { custom: 'object' }; // 返回这个对象,而不是this
}

const myCar = new Car('HQ');
console.log(myCar.brand); // HQ

示例3:new的内部过程

function simulateNew(constructor, ...args) {
  // 1. 创建新的空对象
  const obj = {};
  // 2. 设置原型链:新对象的原型指向构造函数的`prototype`。
  Object.setPrototypeOf(obj, constructor.prototype);
  // 3. 绑定this并执行构造函数
  const result = constructor.apply(obj, args);
  // 4. 返回结果(如果是对象则返回,否则返回obj)
  return result instanceof Object ? result : obj;
}
function Test(value) {
  this.value = value;
}
const testObj = simulateNew(Test, 10);
console.log(testObj.value); // 10

示例4:箭头函数不能作为构造函数

const ArrowConstructor = () => {
  this.value = 42; // 这里的this是词法作用域的this
};

// new ArrowConstructor(); // TypeError: ArrowConstructor is not a constructor

new绑定的关键步骤

  1. 创建新的空对象。
  2. 将新对象的原型指向构造函数的prototype
  3. this绑定到新对象,执行构造函数。
  4. 如果构造函数返回对象,则返回该对象;否则返回新对象。

this绑定决策树

当函数被调用时,JavaScript 引擎按照以下顺序确定 this 的值:

函数被调用时
    ↓
是否使用new调用? → 是 → this = 新创建的对象
    ↓否
是否使用call/apply/bind? → 是 → this = 指定的对象
    ↓否
是否作为对象方法调用? → 是 → this = 调用该方法的对象
    ↓否
严格模式? → 是 → this = undefined
    ↓否
this = 全局对象 (window/global)

特殊情况的处理

回调函数中的this

const controller = {
  data: [1, 2, 3],
  init: function() {
    // 这里this指向controller
    document.addEventListener('click', function(event) {
      // 回调函数中的this指向触发事件的DOM元素
      console.log(this); // 指向被点击的元素,不是controller
      console.log(this.data); // undefined
    });
  }
};

解决方案:保存this引用

const controller2 = {
  data: [1, 2, 3],
  init: function() {
    const self = this; // 保存this
    document.addEventListener('click', function(event) {
      console.log(self.data); // [1, 2, 3]
    });
  }
};

箭头函数的this

// 箭头函数的this
const obj = {
  name: 'zhangsan',
  regularFunc: function() {
    console.log('regular:', this.name); // zhangsan
  },
  arrowFunc: () => {
    console.log('arrow:', this.name); // undefined(或全局的name)
  },
  
  nested: function() {
    // 箭头函数继承外层函数的this
    const arrowInside = () => {
      console.log('nested arrow:', this.name); // zhangsan
    };
    arrowInside();
    
    // 普通函数有自己的this
    const regularInside = function() {
      console.log('nested regular:', this.name); // undefined
    };
    regularInside();
  }
};

obj.regularFunc(); // regular: zhangsan
obj.arrowFunc();   // arrow: undefined
obj.nested();      // nested arrow: zhangsan, nested regular: undefined

常见陷阱与解决方案

陷阱1:方法赋值丢失this

const obj = {
  count: 0,
  increment: function() {
    this.count++;
  }
};

const increment = obj.increment;
increment(); // this指向全局,obj.count不变

解决方案:使用bind

const boundIncrement = obj.increment.bind(obj);
boundIncrement(); // obj.count变为1

陷阱2:回调函数中的this

const processor = {
  process: function(data) {
    data.forEach(function(item) {
      // 这里的this不是processor
      console.log(this.processed); // undefined
    });
  },
  processed: 0
};

解决方案:使用箭头函数或bind

const processor1 = {
  process: function(data) {
    data.forEach(item => {
      // 箭头函数继承外层this
      console.log(this.processed); // 0
    });
  },
  processed: 0
};

this 的记忆口诀:

  • 点调用,左对象
  • call/apply可指定
  • new调用新对象
  • 独立调用看模式(严格/非严格)

思考题

以下代码的输出是什么?为什么?

var name = 'Global';

const obj = {
  name: 'Object',
  getName: function() {
    return function() {
      return this.name;
    };
  }
};

console.log(obj.getName()());

欢迎在评论区分享你的答案和分析!

结语

本文主要介绍了 this 的四种绑定规则与常见陷阱,对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!

❌