网站首页 > 技术文章 正文
本节是第四讲的第二十五小节第二部分,我们继续为大家讲解对象、数组、函数、自定义对象等概念。
对象
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重新介绍之二,更多示例请参见网站示例。跟着张员外讲编程,学习更轻松,不花钱还能学习真本领。
猜你喜欢
- 2024-11-19 Java Java命令学习系列(一)——Jps
- 2024-11-19 langchain 如何提示大模型使用哪个工具函数
- 2024-11-19 ECMAScript和JavaScript有啥区别?
- 2024-11-19 java枚举、反射以及注解,看这一篇就够了
- 2024-11-19 揭秘:Proxy 与 Reflect,为何总是形影不离?
- 2024-11-19 Go 语言反射的实现原理
- 2024-11-19 Java学习中你所不知道的12个常见语法糖详解
- 2024-11-19 了解 JS 的加载顺序和方式,实现 Ready 方法
- 2024-11-19 js 箭头函数
- 2024-11-19 6 款 Java 8 自带工具,轻松分析定位 JVM 问题
- 标签列表
-
- content-disposition (47)
- nth-child (56)
- math.pow (44)
- 原型和原型链 (63)
- canvas mdn (36)
- css @media (49)
- promise mdn (39)
- readasdataurl (52)
- if-modified-since (49)
- css ::after (50)
- border-image-slice (40)
- flex mdn (37)
- .join (41)
- function.apply (60)
- input type number (64)
- weakmap (62)
- js arguments (45)
- js delete方法 (61)
- blob type (44)
- math.max.apply (51)
- js (44)
- firefox 3 (47)
- cssbox-sizing (52)
- js删除 (49)
- js for continue (56)
- 最新留言
-