初试游戏编程
什么是游戏编程?
游戏编程(Gaming Programming)是一种通过编程来实现电子游戏中的逻辑的方法,游戏开发工程师就是通过游戏编程来进行渲染、交互
等操作的
什么是游戏编程语言?
所谓的游戏编程语言,就是编写游戏程序用到的语言,如C编程语言、C++、Java、JavaScript、Lua等
什么是游戏引擎?
游戏引擎是指一些已编写好的可编程电脑游戏系统或者一些交互式实时图像应用程序的核心组件,这些系统为游戏设计者提供各种编写游戏所需的各种工具,其目的在于让游戏设计者能容易和快速地做出游戏程式而不用由零开始。(游戏引擎的作用类似于你写程序的时候用到的库,有了这些库和库函数,你想实现很多功能,就不用从零开始造轮子,而是直接调接口就行了)
大部分游戏引擎都支持多种操作平台,如Linux、Mac OS X、微软Windows。游戏引擎包含以下系统:
- 渲染引擎(即“渲染器”,含二维图像引擎和三维图像引擎)
- 物理引擎
- 碰撞检测系统
- 音效、脚本引擎
- 电脑动画、人工智能、网络引擎
- 场景管理
简单来说,引擎可以理解为一个开发游戏用的基本框架,对于一些游戏开发上通用的功能,不用再重复造轮子了,拿来就用
常见游戏引擎有哪些?
- Unity 3D
- 虚幻引擎
- Cry Engine
- 白鹭
- Cocos 2D
开发前的编程语言准备(JAVAScript)
初始JavaScript
JavaScript是大众熟知的一种前端脚本语言,但因早期商标问题(与Java商标冲突),正式标准名称为ECMAScript。在游戏开发中,我们也经常会用到它作为游戏编程语言
微信环境对ES6的支持
基于安全考虑,小程序中不支持动态执行JS代码,即:
- 不支持使用eval执行JS代码
- 不支持使用newFunction创建函数
let和const关键字
JavaScript在ES6版本引入了新的关键字let和const,旨在解决传统var声明变量带来的作用域混乱、重复声明和变量提升等问题,使JavaScript语言更接近其他主流编程语言的严谨性。引入const和let之后,建议优先使用const(默认不可变),仅当需要重新赋值时使用let,彻底摒弃var声明
let作用域边界测试
实验代码1(var声明):
// 使用var声明循环变量
for(var a = 0; a < 10; a++) {}
console.log(a); // 输出:10(变量穿透循环代码块)
实验代码2(let声明):
// 使用let声明循环变量
for(let b = 0; b < 10; b++) {}
console.log(b); // 报错:Uncaught ReferenceError: b is not defined
结论:let声明的变量严格限制在代码块内,外部访问直接报错,解决了传统var变量"越界访问"问题。
let重复声明冲突测试
实验代码3(var重复声明):
var a = 1;
var a = 2; // 允许重复声明
console.log(a); // 输出:2(后声明覆盖前声明)
实验代码4(let重复声明):
let b = 1;
let b = 2; // 同一作用域内重复声明
// 报错:Uncaught SyntaxError: Identifier 'b' has already been declared
结论:let强制变量名唯一性,在编译阶段即可发现重复声明错误,避免运行时逻辑异常。
let变量提升行为测试
实验代码5(var变量提升):
console.log(a); // 输出:undefined(声明提升,值未初始化)
var a = "apple";
实验代码6(let无变量提升):
console.log(b); // 报错:Uncaught ReferenceError: Cannot access 'b' before initialization
let b = "banana";
结论:let声明的变量存在"暂时性死区"(TDZ),必须先声明后使用,符合大多数编程语言的变量使用规范。
const关键字说明
const与let特性基本一致,唯一区别是const声明的变量必须初始化且不可重新赋值(但对象属性可修改)
当我们使用const 声明一个变量之后,该变量在声明之后不允许改变。并且const声明的变量必须立即初始化,否则会报错。
const PI = "3.1415926";
console.log(PI)
// 输出结果--> 3.1415926
const GENDER;
// 输出结果--> Uncaught SyntaxError: Missing initializer in const declaration
Symbol介绍
Symbol的基本用法
Symbol是ES6版本新增的一种数据类型(其余的数据类型还有Number、String、Boolean、Object、null 和 undefined)
Symbol的功能类似于一种标识唯一性的ID。通常情况下,我们可以通过调用Symbol()函数来创建一个Symbol实例。
let s1 = Symbol()
或者,你也可以在调用symbol()函数时传入一个可选的字符串参数,相当于给你创建的Symbol实例一个描述信息:
let s2 = Symbol('another symbol')
如何理解Symbol的唯一性?
let s1 = Symbol()
let s2 = Symbol('another symbol')
let s3 = Symbol('another symbol')
s1 === s2 // false
s2 === s3 // false
使用Symbol来作为对象属性名
const PROP_NAME = Symbol()
const PROP_AGE = Symbol()
let obj = {
[PROP_NAME]: "一斤代码"
}
obj[PROP_AGE] = 18
运行代码后观察后台,我们就可以看到obj的属性已经初始化完毕了
obj[PROP_NAME] // '一斤代码'
obj[PROP_AGE] // 18
随之而来的是另一个非常值得注意的问题:就是当使用了Symbol作为对象的属性key后,在对该对象进行key的枚举时,会有什么不同?
在实际应用中,我们经常会需要使用Object.keys()或者for…in来枚举对象的属性名,那在这方面,与一般的数据类型相比,Symbol类型的key表现的会有什么不同之处呢?看下面的实验
我们先定义一个包含 Symbol 类型属性和普通属性的对象
let obj = {
[Symbol('name')]: '一斤代码', // Symbol类型的属性名
age: 18, // 普通字符串类型的属性名
title: 'Engineer' // 普通字符串类型的属性名
}
- 这里创建了一个对象
obj,包含 3 个属性:[Symbol('name')]:用 Symbol 作为属性名(注意必须用方括号[]包裹,否则会被识别为字符串),值是'一斤代码';age和title:常规的字符串属性名,是我们日常最常用的形式。
Object.keys(obj)—— 获取对象可枚举的字符串属性名数组
Object.keys(obj) // ['age', 'title']
Object.keys()的作用是返回对象中所有可枚举的、字符串类型的属性名组成的数组;- 结果只返回了
age和title,完全忽略了Symbol('name')这个属性,因为它的属性名是 Symbol 类型,不是字符串。
for...in循环 —— 遍历对象可枚举的字符串属性
for (let p in obj) {
console.log(p) // 分别会输出: 'age' 和 'title'
}
for...in循环的规则和Object.keys()一致,只遍历可枚举的、字符串类型的属性;- 所以循环里只会打印
age和title,依然看不到 Symbol 属性。
Object.getOwnPropertyNames(obj)—— 获取对象所有字符串类型的属性名(无论是否可枚举)
Object.getOwnPropertyNames(obj) // ['age', 'title']
- 这个方法比
Object.keys()更“全”—— 它会返回对象自身的、所有字符串类型的属性名(包括不可枚举的); - 但即便如此,它依然只认字符串属性名,还是忽略 Symbol 属性,所以结果还是
['age', 'title']。
JSON.stringify(obj)—— 把对象转为 JSON 字符串
JSON.stringify(obj) // {"age":18,"title":"Engineer"}
- JSON 格式的规范里没有 Symbol 类型,所以
JSON.stringify()会自动忽略对象中的 Symbol 类型属性; - 最终生成的 JSON 字符串里只有
age和title,没有 Symbol 属性的痕迹。
在上面的实验中,我们先定义了一个包含 Symbol 类型属性和普通属性的对象,然后用 4 种常用的对象属性枚举方法去遍历,结果发现所有常规方法都“看不到”Symbol 类型的属性,这是 Symbol 最核心的特性之一。
之所以给 Symbol 类型的变量定义这个特性,是为了让我们能定义**“隐藏”的对象属性**—— 既可以给对象添加属性,又不用担心这些属性被常规的遍历方法(如 Object.keys、for...in)意外获取到,避免属性名冲突。
总结
- Symbol 作为对象属性名时,不会被
Object.keys()、for...in、Object.getOwnPropertyNames()这些常规方法遍历到,JSON.stringify()也会忽略 Symbol 类型的对象属性; - 这个特性让 Symbol 适合做对象的“私有属性”(或避免属性名冲突)
使用Symbol来替代常量
Symbol类型引入之后,我们可以用它来代替常量
const TYPE_AUDIO = 'AUDIO'
const TYPE_VIDEO = 'VIDEO'
const TYPE_IMAGE = 'IMAGE'
// 使用Symbol来替代常量(Symbol的唯一性)
const TYPE_AUDIO = Symbol()
const TYPE_VIDEO = Symbol()
const TYPE_IMAGE = Symbol()
为什么要用symbol代替常量呢? 其主要目的是利用Symbol的唯一性避免常量值冲突
-
传统常量定义方式:
用字符串作为常量值(如TYPE_AUDIO = 'AUDIO'),存在风险——若其他地方也定义了同名字符串常量,可能出现值重复、逻辑混淆的问题。 -
Symbol替代后的方式:
将常量定义为Symbol()实例(如TYPE_AUDIO = Symbol()),由于每个Symbol都是唯一的,即使多个常量名称相同,它们的实际值也不会重复,彻底避免了常量冲突的问题。
Symbol替代常量的核心优势
- 唯一性:每个Symbol实例互不相等,确保常量的“标识唯一性”;
- 不可篡改:Symbol本身是原始类型,无法被修改,适合作为固定标识的常量。
解构赋值
什么是解构赋值?
结构赋值是ES6引入的赋值运算扩展语法,通过模式匹配机制从数组或对象中提取值并赋值给变量。通过解构赋值,我们可以大大简化变量的赋值过程
数组模型的解构
如何进行解构赋值?(基础语法)
// 基本用法 a=1,b=2,c=3
let [a, b, c] = [1, 2, 3]
// 可嵌套
let [a, [[b], c]] = [1, [[2], 3]];
// 忽略
let [a, , b] = [1, 2, 3];
// 剩余运算符
let [a, ...b] = [1, 2, 3];
// 默认值
let [a = 3, b = a] = []; // a = 3, b = 3
let [a = 3, b] = [4,6]; // a = 4, b = 6
// 当解构模式有匹配结果,且匹配结果是 undefined 时,会触发默认值作为返回结果。
let [a = 2] = [undefined]; // a = 2
基本用法:变量按数组顺序匹配赋值
let [a, b, c] = [1, 2, 3];
结果:a=1, b=2, c=3(三个独立变量)
多层嵌套解构
数组嵌套数组的解构(需模式完全匹配)
let [a, [b], c] = [1, [2], 3];
// 结果:a=1, b=2, c=3
忽略元素
let [a, , b] = [1, 2, 3];
结果:a=1(取第1个),b=3(跳过第2个)
剩余运算符
let [a, ...b] = [1, 2, 3];
结果:a=1,b=[2,3](收集剩余元素为数组)
默认值机制
这个默认值机制就可以理解为C++里面的缺省机制,就是我给这个变量事先写好一个缺省值,如果你在定义变量的时候没有初始化,那我们就将变量初始化为默认值,如果给了一个值,那我们就将变量初始化为给定的值
基础默认值:未赋值时使用默认值
let [a=3, b=a] = [];
结果:a=3, b=3(因右侧无对应值)
undefined特殊处理:显式undefined触发默认值
let [a=2] = [undefined];
结果:a=2(undefined视为未赋值)
解构失败场景:变量值为undefined
let [f] = [];
结果:f=undefined
字符串的解构应用
字符串按字符序列解构(视为类数组对象)
let [a, b, c, d, e] = "hello";
// 结果:a=‘h’, b=‘e’, c=‘l’, d=‘l’, e=‘o’
对象模型的解构
对象解构定义
通过花括号{}声明变量,从对象中提取属性并赋值的语法,本质是数组解构的对象版实现,旨在简化对象属性访问代码。
对象解构语法
let { fo, bu } = { fo: 'aaa', bu: 'bbb' };
解构结果:fo = ‘aaa’,bu = ‘bbb’
嵌套解构实现
处理包含数组和对象的复合结构:
// 源对象
const data = {
p: ['hello', { y: 'word' }]
};
// 嵌套解构
const { p: [x, { y }] } = data;
// 结果:x = 'hello',y = 'word'
- 关键规则:解构模式必须与数据结构完全匹配
- 数组处理:使用
[]对应数组,{}对应对象
忽略与不完全解构
- 忽略中间属性:
const { p: [x] } = data; // 只提取数组第一个元素,忽略对象
- 结构不匹配场景:
const obj = { p: [{ y: 'word' }] };
const { p: [x, { y }] } = obj;
// 结果:x = undefined(无对应值),y = ‘word’(正常解构)
剩余运算符与默认值
- 剩余运算符
const { a, b, ...rest } = { a: 10, b: 20, c: 30, d: 40 };
// 结果:a=10, b=20, rest={c:30, d:40}
- 默认值设置
const { a = 5, b = 10 } = { a: 3 };
// 结果:a=3(使用实际值),b=10(使用默认值)
集中演示
// 基本
let { foo, bar } = { foo: 'aaa', bar: 'bbb' };
// foo = 'aaa'
// bar = 'bbb'
let { baz: foo } = { baz: 'ddd' };
// foo = 'ddd'
// 可嵌套可忽略
let obj = { p: ['hello', { y: 'world' }] };
let { p: [x, { y }] } = obj;
// x = 'hello'
// y = 'world'
let obj = { p: ['hello', { y: 'world' }] };
let { p: [x, { }] } = obj;
// x = 'hello'
// 不完全解构
let obj = { p: [{ y: 'world' }] };
let { p: [{ y }, x] } = obj;
// x = undefined
// y = 'world'
// 剩余运算符
let {a, b, ...rest} = {a: 10, b: 20, c: 30, d: 40};
// a = 10
// b = 20
// rest = {c: 30, d: 40}
// 解构默认值
let {a = 10, b = 5} = {a: 3};
// a = 3; b = 5;
let {a: aa = 10, b: bb = 5} = {a: 3};
// aa = 3; bb = 5;
原生函数常见写法
什么是原生函数?
原生函数是JavaScript中使用function关键字声明的基础函数类型。又可以细分为命名函数、匿名函数和自执行函数三大类
命名函数(具名函数)
顾名思义,具名函数就是具有明确函数名的函数,可独立声明并多次调用
语法结构:
function 函数名(参数列表) {
// 函数体
}
关键特性:
- 可直接调用:通过函数名+()即可执行
- 可重复使用:无调用次数限制
- 函数提升:可在声明前调用(函数声明提升特性)
示例代码:
// 声明命名函数
function f1() {
console.log("命名函数执行");
}
// 调用命名函数
f1(); // 输出:命名函数执行
匿名函数
匿名函数没有函数名,需通过变量引用或立即执行方式调用。
语法结构:
// 方式1:赋值给变量
let 变量名 = function(参数列表) {
// 函数体
};
// 调用方式
变量名()
关键特性:
- 无独立名称:无法直接通过函数名调用
- 需载体执行:必须赋值给变量或作为参数传递
- 建议使用let/const声明匿名函数的接收变量
示例代码:
// 声明并赋值匿名函数
let f2 = function() {
console.log("匿名函数执行");
};
// 调用匿名函数
f2(); // 输出:匿名函数执行
自执行函数(立即执行函数)
自执行函数又叫做立即执行函数,指的是声明后立即自动执行的函数,仅可执行一次
语法结构:
标准写法 (function(参数){...})(实参); 函数体与参数分离包裹
紧凑写法 (function(参数){...}(实参)); 函数体与参数整体包裹
关键特性:
- 自动执行:声明后立即调用,无需手动触发
- 单次使用:无变量引用,执行后立即释放
- 作用域隔离:内部变量不污染全局作用域
示例代码:
// 标准写法
(function(n1, n2) {
console.log(n1 + n2); // 输出:110
})(10, 100);
// 紧凑写法
(function(n1, n2) {
console.log(n1 + n2); // 输出:110
}(10, 100));
箭头函数
什么是箭头函数?
ES6箭头函数(Arrow Function)是ECMAScript 2015标准引入的函数简写语法,通过=>符号定义函数,主要用于简化函数表达式写法。
箭头函数使用=>(等号+大于号)构成"箭头"形状,替代传统function关键字
| 函数类型 | 基本语法结构 | 适用场景 | 代码示例 |
|---|---|---|---|
| 传统函数表达式 | let f = function(x) { return x + x; } | 复杂逻辑、需要命名或绑定this | let add = function(a,b) { return a + b; } |
| 箭头函数-单参数简写 | 参数 => 函数体 | 单个参数、单行返回值 | let double = x => x + x; |
| 箭头函数-多参数完整格式 | (参数1, 参数2) => { 函数体 } | 多个参数、复杂逻辑 | let sum = (a,b) => { let result = a + b; return result; } |
箭头函数参数定义规则
无参数:需使用空括号 () => { … }
单参数:可省略括号 x => x * 2(等效于(x) => x * 2)
多参数:必须使用括号包裹 (a, b, c) => a + b + c
箭头函数函数体书写规范
- 单行隐式返回:
当函数体为单行表达式时,可省略{}和return关键字,表达式结果自动返回
示例:x => x * 2(等效于x => { return x * 2; }) - 多行显式返回:
多行代码必须使用{}包裹,并显式使用return返回值。示例:
let calculate = (a, b) => {
let sum = a + b;
let product = a * b;
return { sum, product }; // 返回对象需加括号:() => ({ key: value })
}
应用案例分析
案例1:基础数值计算
// 单参数箭头函数实现数值翻倍
let double = x => x + x;
console.log(double(100)); // 输出:200(自动返回计算结果)
// 多参数箭头函数实现加法运算
let add = (a, b) => {
let result = a + b;
return result; // 显式返回
};
console.log(add(1, 2)); // 输出:3
案例2:语法对比与转换
将传统函数转换为箭头函数的典型场景:
| 传统函数写法 | 箭头函数等效写法 | 转换要点 |
|---|---|---|
function(x) { return x * 3; } | x => x * 3 | 省略function、{}和return |
function(a,b) { let c = a*b; return c; } | (a,b) => { let c = a*b; return c; } | 仅省略function关键字 |
箭头函数返回值
当箭头函数要返回对象的时候,为了区分于代码块,要用()将对象包裹起来
// 错误写法
let f = (id,name) => {id: id, name: name};
f(18,"播仔"); // SyntaxError: Unexpected token :
// 正确写法
let f = (id,name) => ({id: id, name: name});
f(18,"播仔"); // {id: 18, name: '播仔'}
箭头函数的this
javascript中的this指针和C++类中的this指针有着类似的含义
- 普通函数的this:指向它的调用者,如果没有调用者则默认指向window
- 箭头函数体中的 this 对象,是定义函数时的对象,而不是使用函数时的对象。
- 箭头函数中的this,首先从它的父级作用域中找,如果父级作用域还是箭头函数,再往上找,如此直至找到this的指向
// 全局作用域标识
const str = "window";
// 对象1定义
const obg = {
str: "obg", // 对象1标识
lettuceFn: function() { // 普通函数
console.log("当前词法作用域中的this:", this.str);
return function() { // 返回普通函数
console.log("原生函数 this变量:", this.str);
};
},
arrowFn: function() { // 返回箭头函数的普通函数
return () => { // 箭头函数
console.log("箭头函数 this变量:", this.str);
};
}
};
// 对象2定义(用于call绑定测试)
const obg2 = { str: "obg2" }; // 对象2标识
// 获取函数引用
const lettuceFn = obg.lettuceFn(); // 普通函数引用
const arrowFn = obg.arrowFn(); // 箭头函数引用
// 直接调用(调用者为window)
console.log("----- 函数调用一 -----");
lettuceFn(); // 输出:原生函数 this变量: window
arrowFn(); // 输出:箭头函数 this变量: obg
结果解析:
- 普通函数:this指向调用者window(因无显式调用者)
- 箭头函数:this绑定定义时的作用域(obg对象),与调用者无关
// 使用obg2绑定调用者
console.log("----- 函数调用二 -----");
lettuceFn.call(obg2); // 输出:原生函数 this变量: obg2
arrowFn.call(obg2); // 输出:箭头函数 this变量: obg
结果解析:
- 普通函数:通过call修改调用者为obg2,this随之改变
- 箭头函数:call无法修改其this,仍指向定义时的obg对象
Math函数
随机数生成:Math.random()
基础用法:Math.random()返回一个**[0, 1)区间**的随机小数(包含0,不包含1)。
实现方式:通过浏览器刷新触发,每次返回不同的随机值,适用于随机事件触发、随机道具生成等场景。
代码示例:
const randomValue = Math.random();
console.log(randomValue); // 输出:0.123456...(每次刷新值不同)
数值取整函数
| 函数名 | 中文名称 | 核心逻辑 | 正数示例 | 负数示例 |
|---|---|---|---|---|
| Math.ceil() | 向上取整 | 向正无穷方向取最近整数 | 12.03 → 13(即使小数部分接近0) | -12.9 → -12(因-12 > -13) |
| Math.floor() | 向下取整 | 向负无穷方向取最近整数 | 12.9 → 12(忽略小数部分) | -12.1 → -13(向更小整数取整) |
| Math.round() | 四舍五入 | 向最近整数取整,中间值(.5)向正无穷取整 | 16.3 → 16;16.5 → 17 | -16.4 → -16;-16.5 → -16(中间值向正方向取整) |
数学运算函数
- 绝对值函数:
Math.abs()- 用于获取数值的非负值。 - 三角函数:
Math.asin()(反正弦函数)- 用于角度计算。 - 数学常量:
Math.PI(圆周率π,约等于3.14159)。 - 幂运算:
Math.pow()- 计算数值的指定次幂。 - 平方根:
Math.sqrt()- 计算数值的平方根。
| 函数/常量 | 语法格式 | 核心功能 | 游戏开发应用场景 |
|---|---|---|---|
Math.abs(x) | Math.abs(-12) | 返回x的绝对值 | 计算两点间距离(确保非负) |
Math.PI | Math.PI | 圆周率常量(≈3.14159) | 弧度与角度转换(×180/π) |
Math.asin(x) | Math.asin(y/z) | 反正弦函数(返回弧度) | 计算角色朝向角度 |
Math.pow(x,y) | Math.pow(10,2) | 计算x的y次幂 | 物理引擎中的力/速度计算 |
Math.sqrt(x) | Math.sqrt(100) | 计算x的平方根 | 两点间距离公式(勾股定理) |
游戏开发实战案例:角色瞄准角度计算
- 场景:2D射击游戏中,计算枪手(坐标A)瞄准敌人(坐标B)的角度。
- 坐标定义:
- 枪手A:
[100, 100](x1, y1) - 敌人B:
[200, 200](x2, y2)
- 枪手A:
数学原理与实现步骤
| 步骤 | 数学公式 | JavaScript实现 | 结果说明 |
|---|---|---|---|
| 1.计算直角边 | Δx=x2−x1Δx = x2 - x1Δx=x2−x1 Δy=y2−y1Δy = y2 - y1Δy=y2−y1 | const dx = 200 - 100 = 100const dy = 200 - 100 = 100 | 水平/垂直距离均为100 |
| 2.计算斜边 | z=Δx2+Δy2z = \sqrt{Δx² + Δy²}z=Δx2+Δy2 | const z = Math.sqrt(dx*dx + dy*dy) | 斜边长度≈141.42 |
| 3.反正弦求弧度 | θ(弧度)=arcsin(dy/z)θ(弧度) = arcsin(dy/z)θ(弧度)=arcsin(dy/z) | const rad = Math.asin(dy/z) | 弧度值≈0.7854 |
| 4.弧度转角度 | θ(角度)=(rad/π)×180θ(角度) = (rad / π) × 180θ(角度)=(rad/π)×180 | const angle = (rad / Math.PI) * 180 | 最终角度≈45° |
完整代码示例
// 定义坐标点
const shooter = [100, 100]; // 枪手坐标
const enemy = [200, 200]; // 敌人坐标
// 计算直角边
const dx = enemy[0] - shooter[0]; // x差值:100
const dy = enemy[1] - shooter[1]; // y差值:100
// 计算斜边
const distance = Math.sqrt(dx * dx + dy * dy); // ≈141.42
// 计算角度(弧度转角度)
const radian = Math.asin(dy / distance); // 反正弦值
const angle = Math.round((radian / Math.PI) * 180); // 45°(四舍五入)
console.log("瞄准角度:" + angle + "°"); // 输出:瞄准角度:45°
数据类型Map与Set
Map与Set简介
这个和你在C++中学到的Map和Set的功能是一样的,本质都是保存键值对(key-value pairs)的数据结构。
值得注意的是,在JavaScript中,Object类型也能保存键值对,既然如此,那Object类型与map和set类型有啥区别呢?
- Object仅支持字符串/Symbol作为键的限制
- 而map和set支持任何类型作为键或值。
引入set和map类型的游戏开发价值:适用于存储复杂实体数据(如角色属性、道具信息、场景配置等),满足动态数据管理需求
Map基本操作
- 创建Map实例
const strMap = new Map(); // 初始化空Map
- 存储键值对(set方法)
// 字符串作为键
strMap.set('playerName', 'GameMaster');
// 对象作为键(游戏开发中可用于实体引用)
const playerObj = { id: 1001, level: 30 };
strMap.set(playerObj, 'MainCharacter');
- 获取值(get方法)
const playerName = strMap.get('playerName'); // 返回"GameMaster"
const playerType = strMap.get(playerObj); // 返回"MainCharacter"
- 获取键值对数量
const dataCount = strMap.size; // 返回2(直接属性访问,非函数调用)
应用示例:对象作为键的游戏场景
// 存储游戏道具信息(以道具对象为键,属性为值)
const sword = { id: 'sword_001', damage: 50 };
const shield = { id: 'shield_001', defense: 30 };
const itemMap = new Map();
itemMap.set(sword, { durability: 100, enchant: false });
itemMap.set(shield, { durability: 150, enchant: true });
// 获取指定道具的耐久度
const swordDurability = itemMap.get(sword).durability; // 返回100
注意事项
-
size属性使用:直接通过
map.size访问,而非map.size()(区别于数组的length属性)。 -
键的引用比较:对象作为键时,仅当引用完全相同时才视为同一键(
{}!={})。 -
性能优势:在频繁增删操作场景下,Map性能优于Object(尤其键为非字符串类型时)
-
遍历优势:Map提供
keys()/values()/entries()方法,支持直接迭代键值对,适合游戏循环中的数据遍历需求。 -
内存管理:当使用对象作为键时,若键对象被销毁,对应键值对会自动从Map中移除(弱引用特性)。
Set基本概念
基本概念:Set类型中存储的键值对是不允许重复的,其核心价值在于自动去重和高效的数据唯一性管理
Set的特殊值唯一性规则
对于某些特殊的值,在set中存储依然满足唯一性
| 数据类型 | 唯一性判断规则 | 存储行为 |
|---|---|---|
| 正零(+0)与负零(-0) | 视为恒等(尽管+0 === -0结果为true) | 仅存储一份,最终表现为0 |
| undefined | 严格相等(undefined === undefined) | 仅允许存储一份 |
| NaN(非数值) | 特殊处理:尽管NaN !== NaN,但Set中视为同一值 | 仅允许存储一份 |
数组与Set的相互转换
为什么要让数组与Set相互转换?数组与Set的相互转换有啥用?
数组与Set互转常用于数组去重([...new Set(array)])、数据唯一性校验等场景。
数组转Set
- 语法:
new Set(array) - 原理:Set构造函数接收可迭代对象(如数组),自动过滤重复元素。
- 示例代码:
const originalArray = [1, 2, 3, 3]; // 包含重复元素的数组
const mySet = new Set(originalArray); // 转换为set
console.log(mySet); // 输出:Set(3) {1, 2, 3}(已去重)
Set转数组
- 语法:
[...set](使用扩展运算符) - 原理:扩展运算符可将可迭代对象(如Set)转换为数组。
- 示例代码:
const myArray = [...mySet]; // 将Set转回数组
console.log(myArray); // 输出:[1, 2, 3](数组形式)
数组去重
实现步骤
- 创建Set对象:将目标数组作为参数传入
new Set() - 自动去重:Set构造函数会自动过滤重复元素
- 转回数组:通过扩展运算符(
...)或Array.from()方法将Set转回数组
代码示例
// 原始数组(含重复元素)
const originalArray = [1, 2, 3, 4, 4];
// 步骤1-2:创建Set实现去重
const mySet = new Set(originalArray); // Set {1, 2, 3, 4}
// 步骤3:转回数组
const uniqueArray = [...mySet]; // [1, 2, 3, 4]
console.log(uniqueArray); // 输出:[1, 2, 3, 4](重复的4已被去除)
Set集合运算
| 运算类型 | 数学定义 | 实现方法 | 代码示例 |
|---|---|---|---|
| 并集 | 包含两个集合所有元素(去重) | 将两个Set转数组后合并,再创建新Set | new Set([...setA, ...setB]) |
| 交集 | 包含两个集合共有的元素 | 使用filter() + has()筛选两集合都存在的元素 | new Set([...setA].filter(x => setB.has(x))) |
| 差集 | 包含集合A但集合B没有的元素 | 使用filter() + !has()筛选差异元素 | new Set([...setA].filter(x => !setB.has(x))) |
具体运算实现
基础准备(定义两个Set集合)
// 初始化两个示例集合
const setA = new Set([1, 2, 3]);
const setB = new Set([3, 4, 5]);
- 并集(Union)
const unionSet = new Set([...setA, ...setB]);
// 结果:Set {1, 2, 3, 4, 5}(合并并自动去重)
- 交集(Intersection)
const intersectionSet = new Set([...setA].filter(x => setB.has(x)));
// 结果:Set {3}(仅保留两集合共有的元素)
- 差集(Difference)
const differenceSet = new Set([...setA].filter(x => !setB.has(x)));
// 结果:Set {1, 2}(仅保留setA但setB没有的元素)
ES6类机制与ES5函数嵌套实现对比分析
一句话总结,在ES6之前,JavaScript并没有引入类的概念,因此如果我们想要实现面向对象的程序设计,就必须模拟出类的概念。ES6之前的ES5就是用函数嵌套来模拟出类的概念的。
ES6类的定义
// 类定义(首字母大写规范)
class Point {
// 构造函数(初始化属性)
constructor(x, y) {
this.x = x; // x坐标赋值
this.y = y; // y坐标赋值
}
// 自定义方法
toString() {
return `(${this.x}, ${this.y})`; // 返回坐标字符串
}
}
// 实例化与使用
const pointObj = new Point(100, 200); // 创建对象
console.log(pointObj.toString()); // 输出:(100, 200)
ES5函数嵌套
在ES5中,函数里面是可以定义变量,也可以嵌套定义函数,所以他就能当做一个类来使用
// 构造函数(模拟类)
function createPerson(name) {
// 创建空对象
const obj = new Object();
// 添加属性
obj.name = name;
// 添加方法
obj.showName = function() {
alert(this.name); // 访问对象属性
};
// 返回对象
return obj;
}
// 实例化与使用
const person = createPerson("Bob"); // 创建对象
person.showName(); // 弹窗显示:Bob
2499

被折叠的 条评论
为什么被折叠?



