整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

Javascript入门级教程

Javascript入门级教程

* js的引入方式


附 * HBuilder下载链接


https://www.dcloud.io/hbuilderx.html

1.行内引入

<button id="bt1" onclick="console.log('页面加载成功')">点击实现效果</button>

<button id="bt2" ondblclick="alert('页面加载成功')">点击</button>

2.js内部引入

<script type="text/javascript">

//window表示当前的浏览器页面 onload触发事件进行加载

document.write(Date())

document.write("Hello World ! ! !")

window.onload=function(){

alert('公用的js页面加载成功了!!!')

}

var a=20;

console.log(typeof(a))

var b=document.getElementById('demo')

console.log(b)

//用按钮中的onclick中的函数实现js点击事件

function button(){

alert('点击按钮实现')

}

</script>

<body>

<p id='demo' class='demo'>页面内容展示</p>

<button id='button' onclick='button()'>点击</button>

</body>

3.外部引入

<!--外部引入js页面-->

<script src="js/a.js" type='text/javascript'></script>


二 * 关于js 运算 逻辑

1.js运算


<script type='text/javascript'>

//运算符运算

var a=20;

var b=30;

console.log(a+b)

a++;

b--;

console.log(a)

console.log(b)

var c=50;

c++;

var d=++c;

console.log(c)

console.log(d)

var num=10;

var nums=num++;

console.log(nums)

console.log(num)

</script>

2.三目运算



3.判断语句



4.switch case 选择语句


//switch case 选择语句

console.log('欢迎进入银行管理系统*1.存款 2.取款 3.转账 4.退出')

var choice=parseInt(prompt('请输入选择的行为*'))

switch(choice){

case 1:

put()

break;

case 2:

get()

break;

case 3:

trun()

break;

default:

console.log('退出银行管理系统')

break;

}

function put(){

console.log('选择存款功能')

}

function get(){

console.log('选择取款功能')

}

function trun(){

console.log('选择转账功能')

}

5.for while 语句



6.do while 语句



7.break语句



button onclick语句实例*

JavaScript

JavaScript 是世界上最流行的脚本语言。 JavaScript 是属于 web 的语言,它适用于 PC、笔记本电脑、平板电脑和移动电话。 JavaScript 被设计为向 HTML 页面增加交互性。 许多 HTML 开发者都不是程序员,但是 JavaScript 却拥有非常简单的语法。

JavaScript对大小写敏感。

1.1 JavaScript的用法

HTML 中的脚本必须位于 <script> 与 </script> 标签之间。

脚本可被放置在 HTML 页面的 <body> 和 <head> 部分中。

? <script> 标签

如需在 HTML 页面中插入 JavaScript,请使用 <script> 标签。

<script> 和 </script> 会告诉 JavaScript 在何处开始和结束。

<script> 和 </script> 之间的代码行包含了 JavaScript:

那些老旧的实例可能会在 <script> 标签中使用 type="text/javascript"。现在已经不必这样做了。JavaScript 是所有现代浏览器以及 HTML5 中的默认脚本语言。

? <body> 中的 JavaScript

在本例中,JavaScript 会在页面加载时向 HTML 的 <body> 写文本:

1.2 JavaScript 数据类型

JavaScript 有多种数据类型:数字,字符串,数组,对象等等:

在 JavaScript 中有 5 种不同的数据类型:

l string

l number

l boolean

l object

l function

3 种对象类型:

l Object

l Date

l Array

2 个不包含任何值的数据类型:

l null

l undefined

你可以使用 typeof 操作符来查看 JavaScript 变量的数据类型。

1.3 JavaScript函数

我们把一个 JavaScript 函数放置到 HTML 页面的 <head> 部分:

我们把一个 JavaScript 函数放置到 HTML 页面的 <body> 部分:

也可以把脚本保存到外部文件中。外部文件通常包含被多个网页使用的代码。

外部 JavaScript 文件的文件扩展名是 .js。

如需使用外部文件,请在 <script> 标签的 "src" 属性中设置该 .js 文件:

1.4 JavaScript注释

双斜杠 // 后的内容将会被浏览器忽略。

1.5 JavaScript变量

在编程语言中,变量用于存储数据值。

JavaScript 使用关键字 var 来定义变量, 使用等号来为变量赋值:

1.6 JavaScript操作符

1.7 JavaScript关键字

和其他任何编程语言一样,JavaScript 保留了一些标识符为自己所用。

JavaScript 保留了一些关键字,这些关键字在当前的语言版本中并没有使用,但在以后 JavaScript 扩展中会用到。

JavaScript 标识符必须以字母、下划线(_)或美元符($)开始。

后续的字符可以是字母、数字、下划线或美元符(数字是不允许作为首字符出现的,以便 JavaScript 可以轻易区分开标识符和数字)。

以下是 JavaScript 中最??重要的保留字(按字母顺序):

1.8 JavaScript If...Else 语句

通常在写代码时,总是需要为不同的决定来执行不同的动作。可以在代码中使用条件语句来完成该任务。

在 JavaScript 中,我们可使用以下条件语句:

l if 语句 - 只有当指定条件为 true 时,使用该语句来执行代码

l if...else 语句 - 当条件为 true 时执行代码,当条件为 false 时执行其他代码

l JavaScript三目运算 - 当条件为true 时执行代码,当条件为 false 时执行其他代码

l if...else if....else 语句- 使用该语句来选择多个代码块之一来执行

l switch 语句 - 使用该语句来选择多个代码块之一来执行

1.9 JavaScript for循环

JavaScript 支持不同类型的循环:

l for - 循环代码块一定的次数

l for/in - 循环遍历对象的属性

l while - 当指定的条件为 true 时循环指定的代码块

l do/while - 同样当指定的条件为 true 时循环指定的代码块

1.10 JavaScript JSON

JSON 是用于存储和传输数据的格式。

JSON 通常用于服务端向网页传递数据 。

什么是 JSON?

l JSON 英文全称 JavaScript Object Notation

l JSON 是一种轻量级的数据交换格式。

l JSON是独立的语言 *

l JSON 易于理解。

注:JSON 使用 JavaScript 语法,但是 JSON 格式仅仅是一个文本。文本可以被任何编程语言读取及作为数据格式传递。

以下 JSON 语法定义了 employees 对象: 3 条员工记录(对象)的数组:

{"employees":[

{"firstName":"John", "lastName":"Doe"},

{"firstName":"Anna","lastName":"Smith"},

{"firstName":"Peter", "lastName":"Jones"}

]}

JSON 语法规则:

l 数据为 键/值 对。

l 数据由逗号分隔。

l 大括号保存对象

l 方括号保存数组

JSON 字符串转换为 JavaScript 对象(两种方式):

首先,创建 JavaScript 字符串,字符串为 JSON 格式的数据:

var text='{ "employees" : [' +

'{ "firstName":"John" , "lastName":"Doe" },' +

'{ "firstName":"Anna" , "lastName":"Smith" },' +

'{ "firstName":"Peter" , "lastName":"Jones" } ]}';

l 使用 JavaScript 内置函数 JSON.parse() 将字符串转换为 JavaScript 对象:

var obj=JSON.parse(text);

l JavaScript 函数 eval() 可用于将 JSON 文本转换为 JavaScript 对象。

var obj=eval ("(" + txt + ")");

eval() 函数使用的是 JavaScript 编译器,可解析 JSON 文本,然后生成 JavaScript 对象。必须把文本包围在小括号中,这样才能避免语法错误。

eval() 函数可编译并执行任何 JavaScript 代码。这隐藏了一个潜在的安全问题。

使用 JSON 解析器将 JSON 转换为 JavaScript 对象是更安全的做法。JSON 解析器只能识别 JSON 文本,而不会编译脚本。而且 JSON 解析器的速度更快。

最后,在页面中使用新的 JavaScript 对象:

<p id="demo"></p>

<script>

document.getElementById("demo").innerHTML=

obj.employees[1].firstName + " " + obj.employees[1].lastName;

</script>

使用JSON.stringify方法Javascript对象转换为JSON字符串:

var str={"name":"小牛学堂", "site":"http://www.edu360.cn"}

str_pretty1=JSON.stringify(str)

alert(str_pretty1);

1.11 Javascript void

href="#"与href="javascript:void(0)"的区别:

l # 包含了一个位置信息,默认的锚是#top 也就是网页的上端。

l 而javascript:void(0), 仅仅表示一个死链接。

l 在页面很长的时候会使用 # 来定位页面的具体位置,格式为:# + id。

l 如果你要定义一个死链接请使用 javascript:void(0) 。

<a href="javascript:void(0);">点我没有反应的!</a>

<a href="#pos">点我定位到指定位置!</a>

<br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>

<br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>

<p id="pos">尾部定位点</p>

1.12 Javascript验证表单

JavaScript 可用来在数据被送往服务器前对 HTML 表单中的这些输入数据进行验证。

表单数据经常需要使用 JavaScript 来验证其正确性:

l 验证表单数据是否为空?

l 验证输入是否是一个正确的email地址?

l 验证日期是否输入正确?

l 验证表单输入内容是否为数字型?

必填(或必选)项目

下面的函数用来检查用户是否已填写表单中的必填(或必选)项目。假如必填或必选项为空,那么警告框会弹出,并且函数的返回值为 false,否则函数的返回值则为 true(意味着数据没有问题):

function validateForm()

{

var x=document.getElementById(“”).value;

if (x==null || x=="")

{

alert("姓名必须填写");

return false;

}

}

……

<form name="myForm" action="test.html" onsubmit="return validateForm()" method="post">

姓名: <input type="text" name="fname">

<input type="submit" value="提交">

</form>

E-mail 验证:xxxx@asd.cc

下面的函数检查输入的数据是否符合电子邮件地址的基本语法。

意思就是说,输入的数据必须包含 @ 符号和点号(.)。同时,@ 不可以是邮件地址的首字符,并且 @ 之后需有至少一个点号:

function validateForm(){

var x=document.forms["myForm"]["email"].value;

var atpos=x.indexOf("@");

var dotpos=x.lastIndexOf(".");

if (atpos<1 || dotpos<atpos+2 || dotpos+2>=x.length){

alert("不是一个有效的 e-mail 地址");

return false;

}

}

……

<form name="myForm" action="test.html" onsubmit="return validateForm();" method="post">

Email: <input type="text" name="email">

<input type="submit" value="提交">

</form>

1.13 JavaScript正则表达式

正则表达式(英语:Regular Expression,在代码中常简写为regex、regexp或RE)使用单个字符串来描述、匹配一系列符合某个句法规则的字符串搜索模式。

搜索模式可用于文本搜索和文本替换。

1) 什么是正则表达式?

正则表达式是由一个字符序列形成的搜索模式。

当你在文本中搜索数据时,你可以用搜索模式来描述你要查询的内容。

正则表达式可以是一个简单的字符,或一个更复杂的模式。

正则表达式可用于所有文本搜索和文本替换的操作。

2) 语法

/正则表达式主体/修饰符(可选)

3) 使用字符串方法

在 JavaScript 中,正则表达式通常用于两个字符串方法 : search() 和 replace()。

search() 方法 用于检索字符串中指定的子字符串,或检索与正则表达式相匹配的子字符串,并返回子串的起始位置。

replace() 方法 用于在字符串中用一些字符替换另一些字符,或替换一个与正则表达式匹配的子串。

function myFunction() {

var str="My Test!";

// var n=str.search("Test");

var n=str.search(/test/i);

alert(n);

var str="My Test";

var txt=str.replace(/test/i,"Javascript");

alert(txt);

}

4) 正则表达式修饰符

修饰符 可以在全局搜索中不区分大小写:

i执行对大小写不敏感的匹配。

g执行全局匹配(查找所有匹配而非在找到第一个匹配后停止)。

m执行多行匹配。

5) 正则表达式模式

l 方括号用于查找某个范围内的字符:

[abc]查找方括号之间的任何字符。

[0-9]查找任何从 0 至 9 的数字。

(x|y)查找任何以 | 分隔的选项。

l 元字符是拥有特殊含义的字符:

\d查找数字。

\s查找空白字符。

\uxxxx查找以十六进制数 xxxx 规定的 Unicode 字符。

l 量词:

n+匹配任何包含至少一个 n 的字符串。

n*匹配任何包含零个或多个 n 的字符串。

n?匹配任何包含零个或一个 n 的字符串。

6) 使用 RegExp 对象

在 JavaScript 中,RegExp 对象是一个预定义了属性和方法的正则表达式对象。

test() 方法是一个正则表达式方法。

test() 方法用于检测一个字符串是否匹配某个模式,如果字符串中含有匹配的文本,则返回 true,否则返回 false。

Eg:

/\d/.test(“123”) 返回true。

/^1\d{10}$/

/^0\d{2,3}-?\d{7,8}$/

验证邮箱的正则表达式:

function isEmail(str){

var reg=/^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+(.[a-zA-Z0-9_-])+/;

return reg.test(str);

}

1.14 Javascript高级编程

1.14.1 Javascript对象

1) 创建 JavaScript 对象

通过 JavaScript,能够定义并创建自己的对象。

创建新对象有两种不同的方法:

l 定义并创建对象的实例

l 使用函数来定义对象,然后创建新的对象实例

var person=new Object();

person.firstname="John";

person.lastname="Doe";

person.age=50;

p={firstname:"John",lastname:"Doe",age:50,eyecolor:"blue"};

2) 使用对象构造器

使用函数来构造对象:

function person(firstname,lastname,age,eyecolor){

this.firstname=firstname;

this.lastname=lastname;

this.age=age;

this.eyecolor=eyecolor;

}

var myFather=new person("John","Doe",50,"blue");

alert(myFather.firstname + " is " + myFather.age + " years old.");

在JavaScript中,this通常指向的是我们正在执行的函数本身,或者是指向该函数所属的对象(运行时)。

1.14.2 Javascript Array(数组)对象

1) 什么是数组?

数组对象是使用单独的变量名来存储一系列的值。

2) 创建一个数组,

有三种方法, 下面代码定义了一个名为 myCars的数组对象:

1: 常规方式:

var myCars=new Array();

myCars[0]="Saab";

myCars[1]="Volvo";

myCars[2]="BMW";

2: 简洁方式:

var myCars=new Array("Saab","Volvo","BMW");

3: 字面:

var myCars=["Saab","Volvo","BMW"];

3) 访问数组

通过指定数组名以及索引号码,你可以访问某个特定的元素。

以下实例可以访问myCars数组的第一个值:

var name=myCars[0];

以下实例修改了数组 myCars 的第一个元素:

myCars[0]="Opel";

4) 在一个数组中你可以有不同的对象

所有的JavaScript变量都是对象。数组元素是对象。函数是对象。

因此,你可以在数组中有不同的变量类型。

可以在一个数组中包含对象元素、函数、数组:

myArray[0]=Date.now;

myArray[1]=myFunction;

myArray[2]=myCars;

数组方法和属性

使用数组对象预定义属性和方法:

var x=myCars.length // myCars 中元素的数量

var y=myCars.indexOf("Volvo") // "Volvo" 值的索引值

5) 数组常用方法

合并数组 - concat()

删除数组的最后一个元素 - pop()

数组的末尾添加新的元素 - push()

将一个数组中的元素的顺序反转排序 - reverse()

删除数组的第一个元素 - shift()

从一个数组中选择元素 - slice()

数组排序(按字母顺序升序)- sort()

数字排序(按数字顺序升序)- sort() eg:var arrs=[40,100,1,5,25,10]; arrs.sort(function(a,b){return a-b});

数字排序(按数字顺序降序)- sort() eg:var arrs=[40,100,1,5,25,10]; arrs.sort(function(a,b){return b-a});

转换数组到字符串 -toString()

据类型通常是一门编程语言的基础知识

JavaScript DataType

JavaScript 的数据类型可以分为 7 种:空(Null)、未定义(Undefined)、数字(Number)、字符串(String)、布尔值(Boolean)、符号(Symbol)、对象(Object)。

其中前 6 种类型为基础类型,最后 1 种为引用类型。这两者的区别在于:

基础类型的数据在被引用或拷贝时,是值传递,也就是说会创建一个完全相等的变量;

而引用类型只是创建一个指针指向原有的变量,实际上两个变量是“共享”这个数据的,并没有重新创建一个新的数据。

下面我们就来分别介绍这 7 种数据类型的重要概念及常见操作。

Undefined

Undefined 是一个很特殊的数据类型,它只有一个值,也就是 undefined。

可以通过下面几种方式来得到 undefined:

  • 引用已声明但未初始化的变量;
  • 引用未定义的对象属性;
  • 执行无返回值函数;
  • 执行 void 表达式;
  • 全局常量 window.undefined 或 undefined。

对应代码如下:

var a; // undefined
var o={}
o.b // undefined
(()=> {})() // undefined
void 0 // undefined
window.undefined // undefined

其中比较推荐通过 void 表达式来得到 undefined 值,因为这种方式既简便(window.undefined 或 undefined 常量的字符长度都大于 "void 0" 表达式)

又不需要引用额外的变量和属性;同时它作为表达式还可以配合三目运算符使用,代表不执行任何操作。

如下面的代码就表示满足条件 x 大于 0 且小于 5 的时候执行函数 fn,否则不进行任何操作:

x>0 && x<5 ? fn() : void 0;

如何判断一个变量的值是否为 undefined 呢?

下面的代码给出了 3 种方式来判断变量 x 是否为 undefined,你可以先思考一下哪一种可行。

方式 1 直接通过逻辑取非操作来将变量 x 强制转换为布尔值进行判断;

方式 2 通过 3 个等号将变量 x 与 undefined 做真值比较

方式 3 通过 typeof 关键字获取变量 x 的类型,然后与 'undefined' 字符串真值比较

// 方式1
if(!x) {
  ...
}
// 方式2
if(x===undefined) {
  ...
}
// 方式3
if(typeof x==='undefined') {
  ...// 推荐使用可行
}

现在来揭晓答案,方式 1 不可行,因为只要变量 x 的值为 undefined、空字符串、数值 0、null 时都会判断为真。方式 2 也存在一些问题,虽然通过 “===” 和 undefined 值做比较是可行的,但如果 x 未定义则会抛出错误 “ReferenceError: x is not defined” 导致程序执行终止,这对于代码的健壮性显然是不利的。方式 3 则解决了这一问题。

Null

Null 数据类型和 Undefined 类似,只有唯一的一个值 null,都可以表示空值,甚至我们通过 “==” 来比较它们是否相等的时候得到的结果都是 true,

但 null 是 JavaScript 保留关键字,而 undefined 只是一个常量。

也就是说我们可以声明名称为 undefined 的变量(虽然只能在老版本的 IE 浏览器中给它重新赋值),但将 null 作为变量使用时则会报错。

Boolean

Boolean 数据类型只有两个值:true 和 false,分别代表真和假,理解和使用起来并不复杂。但是我们常常会将各种表达式和变量转换成 Boolean 数据类型来当作判断条件,这时候就要注意了。

下面是一个简单地将星期数转换成中文的函数,比如输入数字 1,函数就会返回“星期一”,输入数字 2 会返回“星期二”,以此类推,如果未输入数字则返回 undefined。

function getWeek(week) {
  const dict=['日', '一', '二', '三', '四', '五', '六'];
  if(week) return `星期${dict[week]}`;
}

这里在 if 语句中就进行了类型转换,将 week 变量转换成 Boolean 数据类型,而 0、空字符串、null、undefined 在转换时都会返回 false。

所以这段代码在输入 0 的时候不会返回“星期日”,而返回 undefined。

我们在做强制类型转换的时候一定要考虑这个问题。

Number

两个重要值

Number 是数值类型,有 2 个特殊数值得注意一下,即 NaN 和 Infinity。

  • NaN(Not a Number)通常在计算失败的时候会得到该值。要判断一个变量是否为 NaN,则可以通过 Number.isNaN 函数进行判断。
  • Infinity 是无穷大,加上负号 “-” 会变成无穷小,在某些场景下比较有用,比如通过数值来表示权重或者优先级,Infinity 可以表示最高优先级或最大权重。

进制转换

当我们需要将其他进制的整数转换成十进制显示的时候可以使用 parseInt 函数,该函数第一个参数为数值或字符串,第二个参数为进制数,默认为 10,当进制数转换失败时会返回 NaN。所以,如果在数组的 map 函数的回调函数中直接调用 parseInt,那么会将数组元素和索引值都作为参数传入。

['0', '1', '2'].map(parseInt) // [0, NaN, NaN]

而将十进制转换成其他进制时,可以通过 toString 函数来实现。

(10).toString(2) // "1010"

精度问题

对于数值类型的数据,还有一个比较值得注意的问题,那就是精度问题,在进行浮点数运算时很容易碰到。比如我们执行简单的运算 0.1 + 0.2,得到的结果是 0.30000000000000004,如果直接和 0.3 作相等判断时就会得到 false。

0.1 + 0.2 // 0.30000000000000004

出现这种情况的原因在于计算的时候,JavaScript 引擎会先将十进制数转换为二进制,然后进行加法运算,再将所得结果转换为十进制。在进制转换过程中如果小数位是无限的,就会出现误差。同样的,对于下面的表达式,将数字 5 开方后再平方得到的结果也和数字 5 不相等。

Math.pow(Math.pow(5, 1/2), 2) // 5.000000000000001

对于这个问题的解决方法也很简单,那就是消除无限小数位。

一种方式是先转换成整数进行计算,然后再转换回小数,这种方式适合在小数位不是很多的时候。比如一些程序的支付功能 API 以“分”为单位,从而避免使用小数进行计算。

还有另一种方法就是舍弃末尾的小数位。比如对上面的加法就可以先调用 toPrecision 截取 12 位,然后调用 parseFloat 函数转换回浮点数。

parseFloat((0.1 + 0.2).toPrecision(12)) // 0.3

String

String 类型是最常用的数据类型了,关于它的基础 API 函数大家应该比较熟悉了,这里我就不多介绍了。下面通过一道笔试题来重点介绍它的使用场景。

位分隔符是指为了方便识别较大数字,每隔三位数会加入 1 个逗号,该逗号就是千位分隔符。如果要编写一个函数来为输入值的数字添加千分位分隔符,该怎么实现呢?

一种很容易想到的方法就是从右往左遍历数值每一位,每隔 3 位添加分隔符。为了操作方便,我们可以将数值转换成字符数组,而要实现从右往左遍历,一种实现方式是通过 for 循环的索引值找到对应的字符;而另一种方式是通过数组反转,从而变成从左到右操作。

function sep(n) {
  let [i, c]=n.toString().split(/(\.\d+)/)
  return i.split('').reverse().map((c, idx)=> (idx+1) % 3===0 ? ',' + c: c).reverse().join('').replace(/^,/, '') + c
}

这种方式就是将字符串数据转化成引用类型数据,即用数组来实现。

第二种方式则是通过引用类型,即用正则表达式对字符进行替换来实现。

function sep2(n){
  let str=n.toString()
  str.indexOf('.') < 0 ? str+='.' : void 0
  return str.replace(/(\d)(?=(\d{3})+\.)/g, '$1,').replace(/\.$/, '')
}

Symbol

Symbol 是 ES6 中引入的新数据类型,它表示一个唯一的常量,通过 Symbol 函数来创建对应的数据类型,创建时可以添加变量描述,该变量描述在传入时会被强行转换成字符串进行存储。

var a=Symbol('1')
var b=Symbol(1)
a.description===b.description // true
var c=Symbol({id: 1})
c.description // [object Object]
var _a=Symbol('1')
_a==a // false

基于上面的特性,Symbol 属性类型比较适合用于两类场景中:常量值和对象属性

避免常量值重复

假设有个 getValue 函数,根据传入的字符串参数 key 执行对应代码逻辑。代码如下所示:

function getValue(key) {
  switch(key){
    case 'A':
      ...
    ...
    case 'B':
      ...
  }
}
getValue('B');

这段代码对调用者而言非常不友好,因为代码中使用了魔术字符串(魔术字符串是指在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或者数值),导致调用 getValue 函数时需要查看函数源码才能找到参数 key 的可选值。所以可以将参数 key 的值以常量的方式声明出来。

const KEY={
  alibaba: 'A',
  baidu: 'B',
  ...
}
function getValue(key) {
  switch(key){
    case KEY.alibaba:
      ...
    ...
    case KEY.baidu:
      ...
  }
}
getValue(KEY.baidu);

但这样也并非完美,假设现在我们要在 KEY 常量中加入一个 key,根据对应的规则,很有可能会出现值重复的情况:

const KEY={
  alibaba: 'A',
  baidu: 'B',
  ...
  bytedance: 'B'
}

这显然会出现问题:

getValue(KEY.baidu) // 等同于 getValue(KEY.bytedance)

所以在这种场景下更适合使用 Symbol,我们不关心值本身,只关心值的唯一性。

const KEY={
  alibaba: Symbol(),
  baidu: Symbol(),
  ...
  bytedance: Symbol()
}

避免对象属性覆盖

假设有这样一个函数 fn,需要对传入的对象参数添加一个临时属性 user,但可能该对象参数中已经有这个属性了,如果直接赋值就会覆盖之前的值。此时就可以使用 Symbol 来避免这个问题。

创建一个 Symbol 数据类型的变量,然后将该变量作为对象参数的属性进行赋值和读取,这样就能避免覆盖的情况,示例代码如下:

function fn(o) { // {user: {id: xx, name: yy}}
  const s=Symbol()
  o[s]='zzz'
  ...
}

补充:类型转换 什么是类型转换?

JavaScript 这种弱类型的语言,相对于其他高级语言有一个特点,那就是在处理不同数据类型运算或逻辑操作时会强制转换成同一数据类型。如果我们不理解这个特点,就很容易在编写代码时产生 bug。

通常强制转换的目标数据类型为 String、Number、Boolean 这三种。下面的表格中显示了 6 种基础数据类型转换关系。


六 种基础数据类型转换关系

除了不同类型的转换之外,操作:

同种数据类型也会发生转换。把基本类型的数据换成对应的对象过程称之为“装箱转换”

反过来,把数据对象转换为基本类型的过程称之为“拆箱转换”

对于装箱和拆箱转换操作,我们既可以显示地手动实现,比如将 Number 数据类型转换成 Number 对象;也可以通过一些操作触发浏览器显式地自动转换,比如将对 Number 对象进行加法运算。

var n=1
var o=new Number(n) // 显式装箱
o.valueOf() // 显式拆箱
n.toPrecision(3) // 隐式装箱, 实际操作:var tmp=new Number(n);tmp.toPrecision(3);tmp=null;
o + 2 // 隐式拆箱,实际操作:var tmp=o.valueOf();tmp + 2;tmp=null;

什么时候会触发类型转换?

下面这些常见的操作会触发隐式地类型转换,我们在编写代码的时候一定要注意。

  • 运算相关的操作符包括 +、-、+=、++、* 、/、%、<<、& 等。
  • 数据比较相关的操作符包括 >、<、==、<=、>=、===。
  • 逻辑判断相关的操作符包括 &&、!、||、三目运算符。

Object

相对于基础类型,引用类型 Object 则复杂很多。简单地说,Object 类型数据就是键值对的集合,键是一个字符串(或者 Symbol) ,值可以是任意类型的值;

复杂地说,Object 又包括很多子类型,比如 Date、Array、Set、RegExp。

对于 Object 类型,我们重点理解一种常见的操作,即深拷贝。

由于引用类型在赋值时只传递指针,这种拷贝方式称为浅拷贝

而创建一个新的与之相同的引用类型数据的过程称之为深拷贝

现在我们来实现一个拷贝函数,支持上面 7 种类型的数据拷贝

对于 6 种基础类型,我们只需简单的赋值即可,而 Object 类型变量需要特殊操作。因为通过等号“=”赋值只是浅拷贝,要实现真正的拷贝操作则需要通过遍历键来赋值对应的值,这个过程中如果遇到 Object 类型还需要再次进行遍历。

为了准确判断每种数据类型,我们可以先通过 typeof 来查看每种数据类型的描述:

[undefined, null, true, '', 0, Symbol(), {}].map(it => typeof it)// ["undefined", "object", "boolean", "string", "number", "symbol", "object"]

发现 null 有些特殊,返回结果和 Object 类型一样都为"object",

所以需要再次进行判断。按照上面分析的结论,我们可以写出下面的函数:

function clone(data) {
  let result = {}
  const keys = [...Object.getOwnPropertyNames(data), 
                ...Object.getOwnPropertySymbols(data)]
  if(!keys.length) return data
  keys.forEach(key => {
    let item = data[key]
    if (typeof item === 'object' && item) {
      result[key] = clone(item)
    } else {
      result[key] = item
    }
  })
  return result
}

在遍历 Object 类型数据时,我们需要把 Symbol 数据类型也考虑进来,

所以不能通过 Object.keys 获取键名或 for...in 方式遍历,

而是通过 getOwnPropertyNames 和 getOwnPropertySymbols 函数将键名组合成数组,

然后进行遍历。对于键数组长度为 0 的非 Object 类型的数据可直接返回,然后再遍历递归,最终实现拷贝。

我们在编写递归函数的时候需要特别注意的是,递归调用的终止条件,避免无限递归

那在这个 clone 函数中有没有可能出现无限递归调用呢?

答案是有的。那就是当对象数据嵌套的时候,比如像下面这种情况,对象 a 的键 b 指向对象 b,对象 b 的键 a 指向对象 a,那么执行 clone 函数就会出现死循环,从而耗尽内存。

var a={
var b={}
a.b=b
b.a=a

怎么避免这种情况呢?一种简单的方式就是把已添加的对象记录下来,这样下次碰到相同的对象引用时,直接指向记录中的对象即可。


要实现这个记录功能,我们可以借助 ES6 推出的 WeakMap 对象,该对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。

我们对 clone 函数改造一下,添加一个 WeakMap 来记录已经拷贝过的对象,如果当前对象已经被拷贝过,那么直接从 WeakMap 中取出,否则重新创建一个对象并加入 WeakMap 中。具体代码如下:

function clone(obj) {
  let map = new WeakMap()
  function deep(data) {
    let result = {}
    const keys = [...Object.getOwnPropertyNames(data), ...Object.getOwnPropertySymbols(data)]
    if(!keys.length) return data
    const exist = map.get(data)
    if (exist) return exist
    map.set(data, result)
    keys.forEach(key => {
      let item = data[key]
      if (typeof item === 'object' && item) {
        result[key] = deep(item)
      } else {
        result[key] = item
      }
    })
    return result
  }
  return deep(obj)
}

总结

本文通过实例与原理相结合,带你深入理解了 JavaScript 的 6 种基础数据类型和 1 种引用数据类型。对于 6 种基础数据类型,我们要熟知它们之间的转换关系,而引用类型则比较复杂,重点讲了如何深拷贝一个对象。其实引用对象的子类型比较多

最后留一道思考题:你能否写出一个函数来判断两个变量是否相等?