Javascript

2019-03-02T12:13:02+08:00 | 8分钟阅读 | 更新于 2019-03-02T12:13:02+08:00

@

学习目标

学完本章你应该能够:

  1. 讲清 JS 的“值类型”和“引用类型”在内存上的本质区别,能预测一段赋值/修改代码的最终输出。
  2. 手写一个递归 deepCopy,正确处理数组、普通对象与嵌套结构,并说清为什么 typeof obj !== "object" || obj === null 要放在最前面。
  3. class / extends / super 写继承,并用 instanceof 判断原型链上的归属关系。
  4. 把“原型 / 原型链”讲成一张图:每个 class 有 prototype,每个实例有 __proto__,属性查找沿着 __proto__ 一路向上。
  5. 说清 this 在不同调用场景(普通函数 / call·apply·bind / 对象方法 / 箭头函数 / class 方法)下到底取什么值,并能手写一个 myBind

前置知识

  • 任意一门编程语言基础(变量、函数、循环即可)
  • 基本的内存概念(栈、堆,不要求深入)
  • 浏览器里跑过最简单的 JS(<script> 或控制台)

本章你会动手做的事

  1. 在控制台跑一遍“值类型 vs 引用类型”两段代码,亲眼看看改 b 会不会影响 a
  2. deepCopy 传一个含数组和嵌套对象的对象,改拷贝后再打印原对象,验证“互不影响”。
  3. 写一个 myBind 并测试:把普通函数的 this 绑到一个对象上,再验证 this.name 是否生效。

一、变量类型和计算

1. typeof 能判断哪些类型

1). 作用

  • 判断所有值类型
// 判断所有值类型
let a;
typeof a; // 'undefined'
const str = "abc";
typeof str; // 'string'
const n = 100;
typeof n; // 'number’
const b = true;
typeof b; // 'boolean'
const s = Symbol("s");
typeof s; // 'symbol'
  • 识别函数
// 能判断函数
typeof console.log         // 'function'
typeof function () {}.     // 'function'
  • 判断是否是引用类型
// 能识别引用类型(不能再继续识别)
typeof null             // 'object'
typeof l'a', 'b']       // 'object'
typeof { x: 100 }       // 'object'

2. 值类型和引用类型

1). 值类型

let a = 50;
let b = a;
a = 100;
console.log(b); // 50

2). 引用类型

let a = { name: "wg" };
let b = a;
b.name = "zhangsan";
onsole.log(a.name); // zhangsan

3). 分类

// 值类型
	undefine  字符串   数值   布尔   Symol
// 引用类型
  obecjt{}  数组[]  null

白话类比:值类型像“复印”——你把 a=50 的纸条复印给 b,之后你在 a 上改成 100,b 那张复印件还是 50,互不影响。引用类型像“共享同一把钥匙”——ab 拿的是同一把钥匙,开的是同一个保险箱,谁进去改了里面的东西,另一个人再开箱看到的也就变了。

这两段代码在内存里其实是这样子的:

flowchart LR
    subgraph 值类型
        VA[a 的格子: 50] -. 拷贝.-> VB[b 的格子: 50]
        VA2[a 改成 100] -. 不影响.-> VB
    end
    subgraph 引用类型
        RA[a] --> BOX[(保险箱 name:wg)]
        RB[b] --> BOX
        RB2[改 b.name] -. 同一箱.-> BOX
    end

3. 深拷贝

function deepCopy(obj) {
  // 步骤 1:如果传入的不是对象或者是 null,则直接返回(也是递归的终止条件)
  if (typeof obj !== "object" || obj === null) {
    return obj;
  }

  let result;
  // 步骤 2:处理数组
  if (Array.isArray(obj)) {
    result = [];
    for (let i = 0; i < obj.length; i++) {
      result[i] = deepCopy(obj[i]);
    }
  } else {
    // 步骤 3:处理普通对象
    result = {};
    for (let key in obj) {
      if (obj.hasOwnProperty(key)) {
        result[key] = deepCopy(obj[key]);
      }
    }
  }

  // 步骤 4:返回全新的拷贝
  return result;
}

// 示例使用
const original = {
  a: 1,
  b: [2, 3],
  c: { d: 4 },
};

const copied = deepCopy(original);
console.log(copied);
copied.b.push(5);
console.log(original.b); // 输出: [2, 3]
console.log(copied.b); // 输出: [2, 3, 5]

二、原型和原型链

1. class 和继承

class 使用 constructor 构建,示例:

class Student {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  SayHello() {
    console.log(`你好,我叫 ${this.name},今年${this.age}岁了`);
  }
}

let student = new Student("zhangsan", 18);
student.SayHello();

继承,使用 extends 关键字来完成继承,super 来执行父类的构造函数,示例:

class Student {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  SayHello() {
    console.log(`你好,我叫 ${this.name},今年${this.age}岁了`);
  }
}

// 继承
class MiddleSchoolStudent extends Student {
  constructor(name, age, task) {
    super(name, age);
    this.task = task;
  }
  CompleteTask() {
    console.log(`${this.name}完成了${this.task}任务`);
  }
}

let middleSchoolStudent = new MiddleSchoolStudent("zhangsan", 20, "英语听力");
middleSchoolStudent.SayHello(); // 你好,我叫 zhangsan,今年20岁了
middleSchoolStudent.CompleteTask(); // zhangsan完成了英语听力任务

2. 类型判断 instanceof

MiddleSchoolStudent instanceof Student     // true
Student instanceof Object      						 // true

[] instanceof Array			// true
[] instanceof Object     // true

{} instanceof Object      // true

3. 原型和原型链

1). 原型

// class 实际上是函数,是个语法糖
typeof Student; // function
typeof MiddleSchoolStudent; // function

// 隐式原型__porto__和显式原型prototype
console.log(MiddleSchoolStudent.__proto__);
console.log(Student.prototype);
console.log(MiddleSchoolStudent.__proto__ === Student.prototype);

image-20250401221944884

  • 每个 class 都有显式原型prototype

  • 每个实例都有隐式原型__proto__

  • 实例的隐式原型 __proto__指向 class 的显式原型prototype

  • MiddleSchoolStudent 获取属性 name 或执行 SayHello()方法时,现在自身属性和方法寻找,如果找不到就到__proto__中去查找

image-20250401223057913

  • MiddleSchoolStudent.hasOwnProperty(’name’) // true
  • MiddleSchoolStudent.hasOwnProperty(‘SayHello’) // false

image-20250401223525675

  • Instancesof 比如 MiddleSchoolStudent instanceof Student,会沿着上述链条寻找

白话类比:原型链像“家族族谱 + 传家宝”。每个班级(class)有一本公共说明书(prototype),每个学生(实例)手里有个指针(__proto__)指向这本说明书。你要用一个方法(比如 SayHello),先翻自己的口袋,没有就顺着指针去翻班级的说明书,再没有就顺着说明书的指针去翻“老祖宗 Object 的说明书”。这条一路往上的查找链,就是原型链。

把这条查找链画成图:

flowchart TD
    I[实例 middleSchoolStudent] -->|__proto__| P1[MiddleSchoolStudent.prototype]
    P1 -->|__proto__| P2[Student.prototype]
    P2 -->|__proto__| P3[Object.prototype]
    P3 -->|__proto__| NULL[null 查找终止]

三、作用域和闭包

1. this 的不同应用场景,如何取值

this 取什么值,是在函数执行时确定的,不是在函数定义的时候确定的

白话类比this 像“这句话是谁说的”。同一个人(函数体)在不同场合(调用方式)下,“我”指代的人不一样:在街上自言自语(普通函数)时“我”指路人甲(window);被介绍给别人(call/bind)时“我”指那个对象;作为某人的秘书(对象方法)时“我”指那位老板;箭头函数则是“沿用上级介绍时的身份”。

1). 作为普通函数

function fn() {
  console.log(this);
}
fn(); // this 是 window

2). 使用 call、apply、bind

// call 是函数对象的一个方法,其用途是改变函数内部 this 的指向,同时能在调用函数时传入参数
function fn() {
  console.log(this);
}
fn.call({ x: 100 });
// this 是 {x:100}

// bind() 是函数对象的一个方法,它主要用于创建一个新的函数,在调用时这个新函数的 this 值会被绑定到指定的对象上,并且可以预设一些参数

const fn1 = fn.bind({ x: 200 });
fn1();
// this 是 {x:200}

3). 作为对象方法被调用

const zhangsan = {
  name: "张三",
  sayHello() {
    console.log(this); // 这里的this指向当前对象
  },
  wait() {
    setTimeout(function () {
      console.log(this); // this === window,这个this是被setTimeout触发执行,所以指向setTimeout,而不是wait触发
    });
  },
};

4). 箭头函数

const zhangsan = {
  name: "张三",
  sayHello() {
    console.log(this); // 这里的this指向当前对象
  },
  wait() {
    setTimeout(() => {
      console.log(this); // this即当前对象,箭头函数被setTimeout触发,但是箭头函数的this取得是箭头函数的上级作用域的this,就是wait()
    });
  },
};

5). 在 class 方法中被调用

class Student{
    constructor(name,age) {
        this.name = name;
        this.age = age;
    }
    SayHello(){
        console.log(this)
    }
}

const zhangsan = New Student("张三",18)
zhangsan.SayHello()       // this指向zhangsan对象

不同场景下 this 的取值,一图概括:

flowchart TD
    A[普通函数 fn] -->|this = window| W[window]
    B[call / apply / bind] -->|this = 指定对象| O[指定对象]
    C[对象方法] -->|this = 当前对象| CUR[该对象]
    D[箭头函数] -->|this = 上级作用域的 this| UP[定义处 this]
    E[class 方法] -->|this = 调用实例| INS[zhangsan]

2. 手写 bind 函数

Function.prototype.myBind = function (thisArg, ...args) {
  // 步骤 1:保存原函数(this 指向被调用的函数)
  const self = this;
  // 步骤 2:返回一个新函数,调用时把 this 绑到 thisArg,并合并预设参数与新参数
  return function (...newArgs) {
    const allArgs = [...args, ...newArgs];
    return self.apply(thisArg, allArgs);
  };
};

// 测试代码
function greet(message) {
  console.log(`${message}, ${this.name}`);
}

const person = { name: "John" };
const boundGreet = greet.myBind(person, "Hello");
boundGreet();

3. 实际开发中闭包的应用场景,举例说明

第一种情况:

函数作为返回值

function create() {
  const a = 100;
  return function () {
    console.log(a);
  };
}

const fn = create();
const a = 200;
fn(); // 输出:100

console.log 中的 a 叫做自由变量,没有被赋值,它会在自己定义的位置向上级作用域查找,不是执行的地方,比如上述例子中,console.log

中的 a 定义在 create()函数中,它会向上查找,找到 const a=100 自己就赋值为 100

第二种情况:

函数作为参数

function print(fn) {
  const a = 200;
  fn();
}

const a = 100;
function fn() {
  console.log(a);
}
print(fn); // 输出:100

console.log 中的 a 为自由变量,在自己定义的作用域中向上级作用域查找,不是执行的地方,最近的是 const a =100 所以输出 100


自测题与动手练习

自测题(合上书能答出来,才算懂)

  1. 值类型和引用类型在内存上有什么区别?为什么改了 b.namea.name 也跟着变?
  2. 手写的 deepCopy 为什么要先判断 typeof obj !== "object" || obj === null?不判断会怎样?
  3. instanceof 是靠什么判断“是否属于某个类”的?它和原型链有什么关系?
  4. 实例访问一个自身没有的属性/方法时,查找顺序是怎样的?__proto__prototype 分别属于谁?
  5. this 在普通函数、对象方法、箭头函数、class 方法里分别取什么值?为什么箭头函数里的 thissetTimeout 回调里的普通函数不一样?

动手练习(建议真做一遍)

  1. 在控制台跑“值类型 vs 引用类型”两段代码,确认改 b 不影响 a、改 b.name 影响 a.name,并用一句话解释原因。
  2. deepCopy 传一个含数组和嵌套对象的对象,改拷贝的数组后再打印原对象,验证两者互不影响。
  3. 写一个 myBind 并测试:把普通函数的 this 绑到 { name: "John" },调用后确认 this.name 生效;再尝试预设参数,验证参数合并顺序。

本章小结

  • 值类型是“复印”(独立格子),引用类型是“共享钥匙”(同一对象),赋值时引用类型只是多了一把指向同一对象的钥匙。
  • deepCopy 的精髓是递归 + 终止条件:typeof obj !== "object" || obj === null 必须最先判断,才能正确处理基本类型和 null
  • class 是函数语法糖,extends/super 完成继承;instanceof 本质是沿着原型链向上查找。
  • 原型链是属性/方法的查找链:实例 __proto__ → class prototype → … → Object.prototypenull
  • this 在函数执行时确定:普通函数指向 window,call/bind 指定对象,对象方法指当前对象,箭头函数沿用上级作用域,class 方法指调用实例。
  • 下一篇我们接着看异步机制(Event Loop、Promise、async/await),把“同步执行”之外的世界讲清楚。
About Me

没什么想介绍的,一个很大众的码农…

喜欢代码,车,马,真的是 🐎

讨厌别人让我给自己的代码写注释 最厌烦别人的程序没有写注释

目标

学AI,加油!加油!