编程技术文章分享与教程

网站首页 > 技术文章 正文

游戏开发之旅-JavaScript重新介绍之二

hmc789 2024-11-19 04:56:09 技术文章 1 ℃

本节是第四讲的第二十五小节第二部分,我们继续为大家讲解对象、数组、函数、自定义对象等概念。

对象

JavaScript 中的对象,Object,可以简单理解成“名称-值”对(而不是键值对,现在,ES 2015 的映射表(Map),比对象更接近键值对),这样的数据结构设计合理,能应付各类复杂需求,所以被各类编程语言广泛采用。正因为 JavaScript 中的一切(除了核心类型,core object)都是对象,所以 JavaScript 程序必然与大量的散列表查找操作有着千丝万缕的联系,而散列表擅长的正是高速查找。“名称”部分是一个 JavaScript 字符串,“值”部分可以是任何 JavaScript 的数据类型——包括对象。这使用户可以根据具体需求,创建出相当复杂的数据结构。

有两种简单方法可以创建一个空对象:

var obj = new Object(); 或 var obj = {};

这两种方法在语义上是相同的。第二种更方便的方法叫作“对象字面量(object literal)”法。这种也是 JSON 格式的核心语法,一般我们优先选择第二种方法。“对象字面量”也可以用来在对象实例中定义一个对象:

var obj = {

name: "Carrot",

for: "Max",//'for' 是保留字之一,使用'_for'代替

details: {

color: "orange",

size: 12

}

}

对象的属性可以通过链式(chain)表示方法进行访问:

obj.details.color; // orange

obj["details"]["size"]; // 12

下面的例子创建了一个对象原型,Person,和这个原型的实例,You。

function Person(name, age) {// 定义一个对象

this.name = name;

this.age = age;

}

var You = new Person("You", 24); // 我们创建了一个新的 Person

完成创建后,对象属性可以通过如下两种方式进行赋值和访问:

obj.name = "Simon"

var name = obj.name;

obj['name'] = 'Simon';

var name = obj['name'];

这两种方法在语义上也是相同的。第二种方法的优点在于属性的名称被看作一个字符串,这就意味着它可以在运行时被计算,缺点在于这样的代码有可能无法在后期被解释器优化。它也可以被用来访问某些以预留关键字作为名称的属性的值:

obj.for = "Simon"; // 语法错误,因为 for 是一个预留关键字

obj["for"] = "Simon"; // 工作正常

注意:从 EcmaScript 5 开始,预留关键字可以作为对象的属性名(reserved words may be used as object property names "in the buff")。 这意味着当定义对象字面量时不需要用双引号了。参见 ES5 Spec.

注意:从 EcmaScript 6 开始,对象键可以在创建时使用括号表示法由变量定义。{[phoneType]: 12345} 可以用来替换 var userPhone = {}; userPhone[phoneType] = 12345 .

数组

JavaScript 中的数组是一种特殊的对象。它的工作原理与普通对象类似(以数字为属性名,但只能通过[] 来访问),但数组还有一个特殊的属性——length(长度)属性。这个属性的值通常比数组最大索引大 1。

//创建数组的传统方法

var a = new Array();

a[0] = "dog";

a[1] = "cat";

a[2] = "hen";

a.length; // 3

//使用数组字面量(array literal)法更加方便

var a = ["dog", "cat", "hen"];

a.length; // 3

//注意,Array.length 并不总是等于数组中元素的个数,如下所示

var a = ["dog", "cat", "hen"];

a[100] = "fox";

a.length; // 101

//记住:数组的长度是比数组最大索引值多一的数。

//如果试图访问一个不存在的数组索引,会得到 undefined

typeof(a[90]); // undefined

//遍历一个数组

for (var i = 0; i < a.length; i++) {

// Do something with a[i]

}

// for...of 循环,可以用它来遍历可迭代对象

for (let currentValue of a) {

// Do something with currentValue

}

// ECMAScript 5 增加了另一个遍历数组的方法,forEach()

["dog", "cat", "hen"].forEach(function(currentValue, index, array) {

// Do something with currentValue or array[index]

});

数组常用方法

a.toString() //返回一个包含数组中所有元素的字符串,每个元素通过逗号分隔。

a.concat(item1[, item2[, ...[, itemN]]]) //返回一个数组,这个数组包含原先 a 和 item1、item2、……、itemN 中的所有元素。

a.join(sep) //返回一个包含数组中所有元素的字符串,每个元素通过指定的 sep 分隔。

a.pop() //删除并返回数组中的最后一个元素。

a.push(item1, ..., itemN) //将 item1、item2、……、itemN 追加至数组 a。

a.reverse() //数组逆序(会更改原数组 a)。

a.shift() // 删除并返回数组中第一个元素。

a.slice(start, end) //返回子数组,以 a[start] 开头,以 a[end] 前一个元素结尾。

a.sort([cmpfn]) //依据可选的比较函数 cmpfn 进行排序,如果未指定比较函数,则按字符顺序比较(即使被比较元素是数字)。

a.splice(start, delcount[, item1[, ...[, itemN]]]) //从 start 开始,删除 delcount 个元素,然后插入所有的 item。

a.unshift(item1[, item2[, ...[, itemN]]]) //将 item 插入数组头部,返回数组新长度(考虑 undefined)。

函数

//最简单的函数

function add(x, y) {

var total = x + y;

return total;

}

这个例子包括你需要了解的关于基本函数的所有部分。一个 JavaScript 函数可以包含 0 个或多个已命名的变量。函数体中的表达式数量也没有限制。你可以声明函数自己的局部变量。return 语句在返回一个值并结束函数。如果没有使用 return 语句,或者一个没有值的 return 语句,JavaScript 会返回 undefined。

add(); // NaN

// 不能在 undefined 对象上进行加法操作

add(2, 3, 4); // 5

// 将前两个值相加,4 被忽略了

已命名的参数更像是一个指示而没有其他作用。如果调用函数时没有提供足够的参数,缺少的参数会被 undefined 替代。

还可以传入多于函数本身需要参数个数的参数。

function add() {

var sum = 0;

for (var i = 0, j = arguments.length; i < j; i++){

sum += arguments[i];

}

return sum;

}

add(2, 3, 4, 5); // 14

函数实际上是访问了函数体中一个名为 arguments 的内部对象,这个对象就如同一个类似于数组的对象一样,包括了所有被传入的参数。让我们重写一下上面的函数,使它可以接收任意个数的参数。

function add(...args) {

var sum = 0;

for (let value of args) {

sum += value;

}

return sum;

}

add(2, 3, 4, 5); // 14

为了使代码变短一些,我们可以使用剩余参数来替换arguments的使用。在这方法中,我们可以传递任意数量的参数到函数中同时尽量减少我们的代码。这个剩余参数操作符在函数中以:...variable 的形式被使用,它将包含在调用函数时使用的未捕获整个参数列表到这个变量中。我们同样也可以将 for 循环替换为 for...of 循环来返回我们变量的值。在上面这段代码中,所有被传入该函数的参数都被变量 args 所持有

需要注意的是,无论“剩余参数操作符”被放置到函数声明的哪里,它都会把除了自己之前的所有参数存储起来。比如函数:function add(firstValue, ...args) 会把传入函数的第一个值存入 firstValue,其他的参数存入 args。

var add= function() {

var sum = 0;

for (var i = 0, j = arguments.length; i < j; i++) {

sum += arguments[i];

}

return sum;

};

这个函数在语义上与 function add() 相同。你可以在代码中的任何地方定义这个函数,就像写普通的表达式一样。

function countChars(elm) {

if (elm.nodeType == 3) { // TEXT_NODE 文本节点

return elm.nodeValue.length;

}

var count = 0;

for (var i = 0, child; child = elm.childNodes[i]; i++) {

count += countChars(child);

}

return count;

}

JavaScript 允许以递归方式调用函数。递归在处理树形结构(比如浏览器 DOM)时非常有用。

var charsInBody = (function counter(elm) {

if (elm.nodeType == 3) { // 文本节点

return elm.nodeValue.length;

}

var count = 0;

for (var i = 0, child; child = elm.childNodes[i]; i++) {

count += counter(child);

}

return count;

})(document.body);

这里需要说明一个潜在问题——既然匿名函数没有名字,那该怎么递归调用它呢?在这一点上,JavaScript 允许你命名这个函数表达式。你可以命名立即调用的函数表达式(IIFE——Immediately Invoked Function Expression)

自定义对象

在经典的面向对象语言中,对象是指数据和在这些数据上进行的操作的集合。与 C++ 和 Java 不同,JavaScript 是一种基于原型的编程语言,并没有 class 语句,而是把函数用作类。那么让我们来定义一个人名对象,这个对象包括人的姓和名两个域(field),名字的表示“名 姓(First Last)”。使用我们前面讨论过的函数和对象概念,可以像这样完成定义:

function makePerson(first, last) {

return {

first: first,

last: last

}}

function personFullName(person) {

return person.first + ' ' + person.last;

}

var s = makePerson("Simon", "Willison");

personFullName(s); // "Simon Willison"

function makePerson(first, last) {

return {

first: first,

last: last,

fullName: function() {

return this.first + ' ' + this.last;

} }

}

s = makePerson("Simon", "Willison");

s.fullName(); // "Simon Willison"

上面的写法虽然可以满足要求,但是看起来很麻烦,因为需要在全局命名空间中写很多函数。既然函数本身就是对象,如果需要使一个函数隶属于一个对象,那么不难得到:

上面的代码里有一些我们之前没有见过的东西:关键字 this。当使用在函数中时,this 指代当前的对象,也就是调用了函数的对象。如果在一个对象上使用点或者方括号来访问属性或方法,这个对象就成了 this。如果并没有使用“点”运算符调用某个对象,那么 this 将指向全局对象(global object)。

下面使用关键字 this 改进已有的 makePerson函数。

我们引入了另外一个关键字:new,它和 this 密切相关。它的作用是创建一个崭新的空对象,然后使用指向那个对象的 this 调用特定的函数。注意,含有 this 的特定函数不会返回任何值,只会修改 this 对象本身。new 关键字将生成的 this 对象返回给调用方,而被 new 调用的函数称为构造函数。习惯的做法是将这些函数的首字母大写,这样用 new 调用他们的时候就容易识别了。

function Person(first, last) {

this.first = first;

this.last = last;

this.fullName = function() {

return this.first + ' ' + this.last;

}

}

var s = new Person("Simon", "Willison");

Person.prototype 是一个可以被Person的所有实例共享的对象。它是一个名叫原型链(prototype chain)的查询链的一部分:当你试图访问 Person 某个实例(例如上个例子中的s)一个没有定义的属性时,解释器会首先检查这个 Person.prototype 来判断是否存在这样一个属性。所以,任何分配给 Person.prototype 的东西对通过 this 对象构造的实例都是可用的。

function Person(first, last) {

this.first = first;

this.last = last;

}

Person.prototype.fullName = function() {

return this.first + ' ' + this.last;

}

这个特性功能十分强大,JavaScript 允许你在程序中的任何时候修改原型(prototype)中的一些东西,也就是说你可以在运行时(runtime)给已存在的对象添加额外的方法:

s = new Person("Simon", "Willison");

Person.prototype.firstNameCaps = function() {

return this.first.toUpperCase()

}

s.firstNameCaps(); // SIMON

有趣的是,你还可以给 JavaScript 的内置函数原型(prototype)添加东西。让我们给 String 添加一个方法用来返回逆序的字符串:

var s = "Simon";

String.prototype.reversed = function() {

var r = "";

for (var i = this.length - 1; i >= 0; i--) {

r += this[i];

}

return r;

}

s.reversed(); // nomiS

正如我前面提到的,原型组成链的一部分。那条链的根节点是 Object.prototype,它包括 toString() 方法——将对象转换成字符串时调用的方法。这对于调试我们的 Person 对象很有用。

var s = new Person("Simon", "Willison");

s; // [object Object]


Person.prototype.toString = function() {

return '<Person: ' + this.fullName() + '>';

}

s.toString(); // <Person: Simon Willison>

apply() 的第一个参数应该是一个被当作 this 来看待的对象。下面是一个 new 方法的简单实现,这并不是 new 的完整实现,因为它没有创建原型(prototype)链。

function trivialNew(constructor, ...args) {

var o = {}; // 创建一个对象

constructor.apply(o, args);

return o;

}

//以下两个语句是等效的

var bill = trivialNew(Person, "William", "Orange");

var bill = new Person("William", "Orange");

apply() 有一个姐妹函数,名叫 call,它也可以允许你设置 this,但它带有一个扩展的参数列表而不是一个数组。

function lastNameCaps() {

return this.last.toUpperCase();

}

var s = new Person("Simon", "Willison");

lastNameCaps.call(s);

// 和以下方式等价

s.lastNameCaps = lastNameCaps;

s.lastNameCaps();

内部函数

function parentFunc() {

var a = 1;

function nestedFunc() {

var b = 4; // parentFunc 无法访问 b

return a + b;

}

return nestedFunc(); // 5

}

JavaScript 允许在一个函数内部定义函数,这一点我们在之前的 makePerson() 例子中也见过。关于 JavaScript 中的嵌套函数,一个很重要的细节是,它们可以访问父函数作用域中的变量:

如果某个函数依赖于其他的一两个函数,而这一两个函数对你其余的代码没有用处,你可以将它们嵌套在会被调用的那个函数内部,这样做可以减少全局作用域下的函数的数量,这有利于编写易于维护的代码。

这也是一个减少使用全局变量的好方法。当编写复杂代码时,程序员往往试图使用全局变量,将值共享给多个函数,但这样做会使代码很难维护。内部函数可以共享父函数的变量,所以你可以使用这个特性把一些函数捆绑在一起,这样可以有效地防止“污染”你的全局命名空间——你可以称它为“局部全局(local global)”。虽然这种方法应该谨慎使用,但它确实很有用,应该掌握。

以上内容部分摘自视频课程04网页游戏编程JavaScript-25重新介绍之二,更多示例请参见网站示例。跟着张员外讲编程,学习更轻松,不花钱还能学习真本领。


Tags:

标签列表
最新留言