Front-end Design(1)
写在前面
本章节的产生原因在于博主需要参加的大创(全称: 大学生创新创业竞赛)项目需要相关的前端界面设计, 而博主恰好负责这一部分较多, 因此在此新开一坑, 衔接此前HTML&CSS的章节, 并在此之上继续向前, 目的在于在博文的最后能够设计出一个较为完整的前端界面出来.
1. JavaScript 概述
JS, 全称 JavaScript, 是一个使用十分广泛, 应用较广的 脚本语言 , 被广泛运用在前端的设计上, 通过JS的种种 行为 , 能够实现前端界面的动画, 以及一些较为合理的交互逻辑.
1.1 ECMAScript & JavaScript
通俗而言, ECMAScript(后文简称ES)是由国际组织(ECMA)指定的一个脚本语言规范, 而JavaScript(后文简称JS)是由Netscape公司开发的, 基于 ES 的脚本语言.
ES的广为流传的版本为ES6, 它于2015年发布.
1.2 JS 语句 / 标识符
JS作为单独的脚本语言, 它有其自己的一套语法, 一套单独的规范.
JS的标识符可以容纳的字符包括 字母, 数字, 下划线, $ 这四种, 数字不能用在标识符开头.
除此之外, 其实中文也可以作为标识符, 但本文中不推荐这样做. 中文进入代码行中出现的各种编码问题, 适配问题已经能搞得博主焦头烂额了.
除此之外, JS也有一套关键字, 类似于C中的 break, return, … , 它们有自己的作用, 因此不能作为单独的标识符使用. 这些关键字我们会在后文中的使用中慢慢引出, 读者在此大可不必过于在意.
1.3 变量
在JS中, 变量可以非常简单的利用统一的符号进行定义:
<script>
var num = 10;
</script>
关于这个前后的<script>, 有HTML基础的读者应该看出这是个标签了, 确实, 由于JS的重要应用就是前端的设计, 因此常常需要插在HTML中进行书写, 具体方式即在相应的html文件中插入对应的标签即可. 后文会将这前后的标签省略, 还请读者记住这个事情.
1.3.1 变量的监视
但不同于C, Java的编译器提供的良好环境, 我们如何监视JS中的各种变量呢? 一个比较直接的方式是通过控制台显示出来.
var num = 10;
console.log(num);
通过这样的语句, 我们可以通过浏览器中 检查->控制台 的界面查看变量的当前值.
1.3.2 变量提升
在JS中, 为了增加兼容性, 会将脚本中所有声明的变量提前至代码运行前优先声明.
这句话不是很好理解, 我们举个例子:
console.log(num);
var num = 10;
如果以C语言输出的角度来看, 上面这段代码应当报错, 但实际上, 在浏览器中显示的状态:
相当于, 浏览器在运行这段脚本时, 其实相当于这样:
var num;
console.log(num);
num = 10;
这即所谓变量提升的意义.
在当今的开发过程中, var的使用已经比较少, 这是因为var变量提升这个特性往往会引起一些额外的错误, 因此ES6中引入了新的变量声明方式, 即let和const.
let, const与var的区别在于, let和const声明的变量不会进行变量提升, 这代表着开发者可以 更加稳定的控制变量的作用域 .
console.log(num);
let num = 10;
上述代码将会报错, 因为let声明的变量不会进行变量提升.
1.4 JS的引入
与CSS类似, JS除开在HTML中单独书写外, 还可以通过外部文件引入的方式进行.
<script type = "text/javascript" src = "./my_first_js.js"></script>
<script src = "http://..."></script>
上述内容分别代表了从本地引入js, 以及从网络外部连接引入js的方法.
1.5 JS的注释
JS中的注释可以通过两种方式进行:
// 这是一条注释
/*
这是一条多行注释
*/
明确, 这种注释是只能在JS中进行的. 相对应, 读者别忘记HTML与CSS中的注释形式.
当然, 比较方便的快捷键是 Ctrl + /
1.6 JS的输出
这里简要提供三种方式
// Method 1
alert("This is Method 1.");
// Method 2
Document.write("This is Method 2.");
// Method 3
console.log("This is Method 3");
第一种方式以弹出框的形式来输出内容, 用户可见:
第二种方式在页面上单独显示一行字体, 用户同样可见:
第三种方式我们之前说过了, 在控制台中进行相关输出, 用户不可见:
1.7 JS的数据类型
1.7.1 原始数据类型
原始数据类型, 也被称为基本数据类型, 一般有三个:
- 数值
- 字符串
- 布尔值
var number = 10;
var str = "string";
var bool = false;
1.7.2 合成数据类型
合成数据类型, 也被称为复合数据类型, 一般由多个原始数据类型组成(C中的结构体):
var user = {
name: "Mug-chen",
age: 1,
admin: true
}
注意, 变量之间通过 , 隔开
1.7.3 特殊类型
null
undefined
1.7.4 类型判断
在数据类型的判断上, 常用typeof函数
var num = 123;
console.log(typeof num); //number
var str = "string";
console.log(typeof str); //string
var boo = true;
console.log(typeof boo); //boolean
var user = {};
console.log(typeof user); //object
//两个特殊类型的结果
console.log(typeof undefined); //undefined
console.log(typeof null); //object
null通常表示对象为空; undefined一般表示数值为空.
1.8 运算符
整体的运算符与C语言基本相同, 这里仅提及不同处.
1.8.1 除法
当然, 这里的除法是可以直接算出小数的, 不会像C, java那样整数除以整数只能得到整数解.
1.8.2 严格等于 & 严格不等于
在JS中, 等于运算符 “==” 出现了一个衍生, 叫 严格等于 “===”, 二者的区别在于, 前者会先进行类型转换再比较, 后者不会:
console.log(5 == "5") \\true
console.log(5 === "5") \\false
类似的, 也有 != 与 !== 的使用, 我们推荐使用 严格等于 / 严格不等于 .
1.8.3 布尔运算
这里主要提及取反 ! , 因为实际应用过程中常常对非布尔值进行取反(主要是方便), 我们这里给出6个取反后为真的例子.
!false;
!0;
!null;
!undefined;
!""; //空字符串取反
!NaN;
1.9 语句
1.9.1 条件语句
给三种
// if_else
if(){
}else{
}
//elseif
if(){
}else if(){
}else if(){
}else{
}
//switch
switch(var_name){
case 1:
...
break;
case 2:
...
break;
default:
...
break;
}
可见, 与C / Java极其类似.
1.9.2 三元运算符
即C / Java中都有的特殊的条件判断:
1>2 ? console.log(true): console.log(false);
1.9.3 循环语句
for(var i = 0;i<100;i++){
console.log(i);
}
while(){
...
}
break;
continue;
1.10 字符串
字符串与Java中类似, 换行需要利用 \
1.10.1 字符串常用方法
(1) charAt
//指定位置字符
var str = "Hello World."
console.log(str.charAt(3));
(2) concat
//字符串连接
var str1 = "Hello";
var str2 = "World";
var str = str1.concat(str2);
//可以有多个参数, 参数之间利用逗号隔开
var str3 = "!";
var s = str1.concat(str2, str3);
当然, 熟悉java的读者, 应该知道, 直接用+也可以连接字符串, 也没毛病.
那用concat有什么区别?
事实是, +使用类型强转, 而concat调用它们的 toString() 方法.
一般情况下区别不大就是了.
var num = 1
var grade = "grade: "
var str = grade.concat(num);
(3) substring
//子字符串
var str = "Hello World";
var sub = str.substring(2, 6); //llo
//第二个参数可不写
var sub_1 = str.substring(2); //llo World
//会自动排序两个参数的大小
var sub_2 = str.substring(9, 2);
//相当于
var sub_2 = str.substring(2, 9);
//负数会被自动转化成0
var sub_3 = str.substring(9, -2);
//相当于
var sub_3 = str.substring(0, 9);
与Java中相同, 含前不含后.
(4) substr
//子字符串, 但第二个参数是字符串长度
var str = "Hello World";
var sub = str.substr(2, 4); //llo
//第二个参数不写, 则一直取到最后
//第一个参数写成负数, 则取逆序的字母值
//第二个参数写成负数, 则自动置为0(空字符串)
var sub = (-2, 1) //l
(5) indexOf
//查看一个字符串在另一个字符串中第一次出现的位置, 如果找不到返回0
var str = "Hello World";
var sub = "Wor";
console.log(str.indexOf(sub)); //6
//可以接受第二个参数, 表示开始查找的位置
console.log(str.indexOf(sub, 7)); //-1
(6) trim
//用于去除字符串两端的特殊字符, 包括空格, 制表符\t, \r, 回车符\n等
var str = " Hello World ";
var res = str.trim();
console.log(res);
//ES6新增扩展方法:
trimStart() //去掉头部特殊字符
trimEnd() //去掉尾部特殊字符
(7) split
//字符串分割函数, 返回一个由子字符串组成的数组
var str = "Hello World Here Is My Blog";
console.log(str.split(" ")); //["Hello", "World", "Here", "Is", "My", "Blog"]
//可以进行空字符串分割, 返回一个由字符串中每个字符单独组成的字符串组成的数组
console.log(str.split(""));
//省略参数, 则返回原字符串组成的数组
console.log(str.split());
//可以接受第二个参数, 限定返回数组的最大成员数
console.log(str.split("", 3)); //["H", "e", "l"]
上面是主要使用的字符串方法, 但其实JS中字符串方法远不止这么多, 希望读者使用时可以有限查一查有没有对应的函数. 毕竟直接调库比手搓一个函数可方便多了(笑)
1.11 数组
var arr = ['Hello', 'World', '!'];
在JS中, 数组的声明一般直接使用var进行. 不同的是, 由于没有了类型的区分, 因此一个数组中可以存在各种类型的数据(整数 / 浮点数 / 字符串 / 数组).
1.11.1 数组操作
(1) 求长
var arr = ['', '', '', ''];
console.log(arr.length);
(2) 遍历
//传统的遍历方法(即利用for / while进行循环)在此不在叙述
var arr = ['1', '2', 3, false]
for(var i in arr){
console.log(arr[i]);
}
(3) 判断类型
var arr = [];
console.log(Array.isArray(arr)); //返回一个布尔值
(4) 快捷添加 / 删除元素
var arr = [];
arr.push(1, 'World', false); //在数组后方添加一条或多条数据
var temp = arr.pop(); //删除数组的最后一个元素, 并返回它
var temp = arr.shift(); //删除数组的第一个元素, 并返回它
关于 shift 这个方法, 有一种很常见的写法, 即利用它来清空数组(顺便正序遍历也可以):
//var arr = [...];
var temp;
while(temp = arr.shift()){
console.log(temp);
}//当数组为空时, 相当于将一个"undefined"赋值给temp, 会返回false, 循环自然结束
相对应的, 也可以快捷的向头部添加元素:
//var arr = [...];
arr.unshift(1, false, "World"); //向头部添加一个或多个元素
(5) 将数组输出
方法 join 可以指定分隔符来将整个数组转变为字符串输出.
var arr = [10, 20, 30, 40];
console.log(arr.join()); //默认用逗号
console.log(arr.join(" ")); //用空格
联系前面提供的字符串split方法, 可以将数组和字符串实现轻松的互相转换.
(6) 数组合并
var arr1 = [1, 2, 3];
var arr2 = ['world', 'hello'];
//var arr3 = ...
var temp = arr1.concat(arr2, arr3, ...);
//可以将一个或多个数组的内容合并到一起并返回, 不会改变原数组
(7) 逆序
var arr1 = [1, 2, 3];
arr.reverse(); //逆序排列原数组, 会改变原数组
这种方法可以用于翻转字符串, 具体而言, 先将字符串转为数组, 再用reverse反转数组, 最后转为字符串即可.
(8) 查找
与字符串相同, 查找某一个元素在数组中第一次出现的位置, 不存在则返回-1;
不再详述.
1.12 函数
在JS中, 函数的声明如下:
function function_name(x, y, z){
...
return ...;
}
//利用function进行声明, 可以传入参数, 并传出返回值
//亦或通过函数表达式的方法进行声明
const function_name = function(x, y, z){
...
return ...;
}
上两种方法都好理解(对于此前有编程基础的读者而言), JS中额外引入了箭头函数:
const function_name = (x, y, z) => {
...
return ...;
}
//箭头函数的语法, 可以省略function关键字, 并将函数体写在箭头后侧
//此外, 如果箭头函数只有一个参数, 可以省略参数列表的括号
const function_name = x => {
...
return ...;
}
//如果箭头函数的函数体只有一行, 可以省略花括号, 并将return关键字一并省略
const function_name = x => ...;
//最后, 如果箭头函数的返回值只有一个, 可以省略return关键字, 并将返回值直接写在箭头后侧
const function_name = x => x+1;
要明确, 箭头函数简略写返回值时, 无法返回用{}包裹的对象, 因为JS会将{}解析为函数体, 而不是对象.
1.13 对象
对象是一个非常重要的概念, 同时这里与Java中的一些语法差距较大, 我们重点叙述一下:
类似Java, 对象中包含属性和方法.
var user = {
name: "Mug-chen",
job: "",
age: "?",
...
getName:function(){
return this.name;
}
}
与Java中的对象极其类似, 只不过声明方法时需要通过 方法名:function 进行声明, 同时属性之间通过 , 隔开即可.
此外, 还可以在对象内套对象:
var Reader{
Re1{
name: "Bob",
age: "18",
...
},
Re2{
...
}
}
可以通过多层引用进行信息调用.如: Reader.Re1.name ;
此外, 如果希望检查对象中是否包含某个属性, 可以使用 in 运算符, 可以返回一个布尔值:
if("name" in user){
...
}
事实上, 在JS中, 属性名可以非常的复杂, 但此时使用属性名时不能使用一般的 . 进行引用, 而需要使用 [] 进行引用, 如:
var user = {
name: "Mug-chen",
job: "",
age: "?",
12345@#$%: "???"
getName:function(){
return this.name;
}
}
user["12345@#$%"]; //调用时需要使用[]进行引用
使用 [] 来进行属性的添加和修改与使用.有什么不同?
除了上文中提到的, [] 可以用于非常复杂的属性名外, 其还可以提取变量名来进行属性的定义:
var obj = new Object();
var str = "name";
obj[str] = "Mug-chen";
console.log(obj.name); //Mug-chen, 这里的属性名为变量str的值, 即name
//假设更改为obj.str = "Mug-chen", 则相当于添加了一个属性名为 "str" 的属性, 属性值为"Mug-chen"
此时, obj[str] 等价于 obj[“name”], 也就是 obj.name.
1.13.1 通过Symbol添加属性
Symbol是ES6中引入的一种新的数据类型, 可以用于创建独一无二的属性名.
在ES6中, 可以通过Symbol来添加属性, 具体如下:
var user = new Object();
var mysymbol = Symbol();
user[mysymbol] = "Mug-chen";
console.log(user[mysymbol]); //Mug-chen
通常来说, 使用Symbol添加属性的目的在于不让外界访问到该属性, 因为Symbol是独一无二的, 外界无法通过常规方式访问到该属性.
1.14 类
在ES6中, 对于 类 这个概念有了引入.
class User{
constructor(name){
this.name = name;
}
getName(){
return this.name;
}
addAge(age){
this.age = age;
}
getAge(){
return this.age;
}
}
var User1 = new User("Alice");
console.log(User1.getName());
User1.addAge(13);
console.log(User1.getAge());
与Java显著的不同在于, JS中不需要再类前明确声明类的所有属性, 而是通过构造器(constructor)或其他方法直接添加即可.
1.15 继承 / 多态 / 原型
JS中, 同样有类继承的操作, 同样使用 extends 关键字:
class Student extends User{
constructor(name, age){
super(name);
this.addAge(age);
}
}
在JS中, 类的继承是单继承的, 即一个类只能继承自一个父类.
类似的, 多态我们此前也提过, 意为在子类中可以重写父类的方法(包括构造方法).
class Student extends User{
constructor(name, age){
super(name);
this.addAge(age);
}
getName(){
return "Student: " + super.getName();
}
}
请明确, 当重写子类的构造函数时, 第一行必须是super(), 即调用父类的构造函数, 这点的意义在于子类想要构造一个实例, 必须首先构造一个父类实例.
原型
在JS中, 直接通过console的方式打印一个对象时, 只会打印对象的一部分属性, 这被称为对象自身的属性, 包含:
- 直接通过对象添加的属性
- 在类中通过x = y的方式添加的属性(包括通过函数表达式书写的函数)
但还有另一类对象不会显式地出现在console中, 这就是 原型对象 . 原型对象存储的位置(内存)与对象自己并不一定相邻, 源对象可以通过 __proto__ 的方式来引用原型对象中的内容, 或者通过方法: Object.getPrototypeOf(obj) 来获取原型对象.
- 当访问对象中的内容时, 会先从对象自身查找, 如果找不到, 则会去原型对象中查找
- 原型对象中存储的属性:
- 类中通过fun_name(){}添加的方法
- 通过this.x = y的方式添加的属性
- 对象的构造函数constructor
原型链
显然, 原型对象也是对象, 也就有原型对象的原型对象…(套娃.jpg), 这一条捋下去, 就能形成一个原型链(最终一定会捋到 null ), 也就是JS中对象继承的原理(我们称之为 原型继承 ).
因此, 我们具象化对象调用属性的过程:
- 先从对象自身查找
- 如果找不到, 则去原型对象中查找
- 如果找不到, 则去原型对象的原型对象中查找
- 直到找到Object.prototype(因为Object.prototype的原型对象是null), 如果仍然找不到, 则返回undefined
为啥要有原型这个玩意?
在JS中, 同类型对象的原型对象是相同的, 因此, 当我们通过原型对象来添加方法时, 所有的对象都可以调用这个方法, 而不必为每个对象都添加一次, 这就是原型链的意义.
2. JS与网页的交互: DOM
DOM是Document Object Model的缩写, 意为文档对象模型, 是一种用于表示和操作HTML或XML文档的编程接口.
事实上, DOM属于Web API的一部分, 它提供了许多方法, 可以通过这些方法来访问和操作HTML文档中的元素.
DOM的原理在于将整个HTML文件视作一整个对象树, 而HTML中的每个标签都是树的一个节点, JS通过DOM对于HTML中的节点进行访问 / 操作, 达到控制HTML行为的效果.
2.1 DOM的入口节点(文档节点): document
document是DOM的入口节点, 代表整个HTML文档, 可以通过document来访问HTML中的所有元素.
通过此前提及的 原型 相关内容, 我们可以逐级回溯到document的原型链:
$$ HTMLDocument -> Document -> Node -> EventTarget -> Object.prototype $$
为什么要梳理这条原型链?
有这条原型链, 代表着这一条链上的所有属性 / 方法, 对于document对象都是可用的. 这也代表着读者此后遇到不理解的内容, 有迹可循.
部分属性举例:
- document.documentElement: 返回文档的根元素, 即标签
- document.body: 返回文档的元素
- document.title: 返回文档的标题
- document.URL: 返回文档的URL
- …
2.2 DOM的元素节点: element
element是DOM的元素节点, 代表HTML中的每一个标签, 可以通过element来访问和操作HTML中的标签.
2.2.1 元素节点的获取
- 通过document对象来获取元素节点
- document.getElementById(“id”): 通过id来获取 单个 元素节点
- document.getElementsByClassName(“class”): 通过类名来获取 一组 元素节点;
- 返回一个 类数组对象 , 这代表这个返回的对象 不能用数组的某些方法 , 比如forEach等…
- 返回的这个类数组对象是 实时更新的 , 代表着查询一次后, 数组中会自动将后续添加的符合要求的元素加进去
- document.getElementsByTagName(“tag”): 通过标签名来获取 一组 元素节点
- 类似, 返回类数组 / 实时更新
- 可以通过 document.getElementByTagName(“*”) 来获取文档中的所有节点
- document.querySelectorAll(“.class”): 通过CSS选择器来获取 一组 元素节点
- 引号内为一个 Css选择器
- 返回一个类数组
- 不会实时更新
- document.querySelector(“.class”): 通过CSS选择器来获取 单个 元素节点
- 如果文档中有多个符合要求的元素, 则之后返回第一个符合条件的元素
- 通过document对象来创建元素节点
- document.createElement(“tag”): 通过标签名来创建一个元素节点
2.2.2 元素节点的操作
我们相似的捋一下元素节点的原型链, 以div为例:
$$ HTMLDivElement -> HTMLElement -> Element -> Node -> … $$
1.通过元素节点查找 范围内的符合要求的节点
这一点与2.2.1几乎一致, 只需要将document更改为对应的对象即可, 不再赘述.
2.获取其他节点
- element.childNodes : 获取element的所有子节点(不常用)
- 返回一个类数组对象
- 类数组对象中包含所有子节点, 包括文本节点和注释节点(换行等空白节点也会包括进去)
- 还有一些fistchild, lastchild等属性, 由于不实用, 这里不再详述
- element.children : 获取element的所有子元素节点
- 返回一个类数组对象
- 类数组对象中只包含子元素节点
- firstElementChild: 获取第一个子元素
- lastElementChild: 获取最后一个子元素
- nextElementSibling: 获取下一个兄弟元素
- previousElementSibling: 获取上一个兄弟元素
- parentElement: 获取父元素
- element.tagName : 获取当前元素的标签名
3.修改元素内文本
- element.innerHTML : 获取或设置元素内的HTML内容
- 可以读取到元素内的所有内容(包括标签)
- 利用innerHTML插入内容时, 有被xss注入的风险
- element.textContent : 获取或设置元素内的文本内容
- 只能读取到元素内的文本内容(不包括标签), 代表着不会考虑css样式, 显示标签中的原始结果
- 插入的内容会被转义
- element.innerText : 获取或设置元素内的文本内容
- 会忽略掉元素内的空白节点(包括换行等空白节点), 会考虑css样式(比如一个标签中有display: none的css设置, 则这个标签的内容不会被innerText读取到)
- 插入的内容会被转义