JavaScript-浏览器

浏览器对象

window

window对象不但充当全局作用域,而且表示浏览器窗口

1
console.log('window inner size: ' + window.innerWidth + ' x ' + window.innerHeight)
  • innerWidth/innerHeight:浏览器窗口的内部宽度和高度
  • 内部宽高是指,去掉菜单栏、工具栏、边框等占位元素后,用于显示网页的净宽高.
  • 对应的有outerWidth/outerHeight,是浏览器整个窗口的宽高

navigator对象表示浏览器信息,常用属性包括:

  • navigator.appName: 浏览器名称
  • navigator.appVersion: 版本
  • navigator.language: 语言
  • navigator.platform: 操作系统类型
  • navigator.userAgent: 浏览器设定的User-Agent字符串
1
2
3
4
5
6
7
8
9
10
11
12
console.log('appName = ' + navigator.appName);
console.log('appVersion = ' + navigator.appVersion);
console.log('language = ' + navigator.language);
console.log('platform = ' + navigator.platform);
console.log('userAgent = ' + navigator.userAgent);

// 输出
appName = Netscape
appVersion = 5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36
language = zh-CN
platform = Win32
userAgent = Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36

注意:navigator的值很容易被用户修改,所以JS读取出来的值不一定正确.另外很多初学者喜欢针对不同的浏览器写不同的代码,喜欢用if判断浏览器版本:

1
2
3
4
5
6
let width;
if (getIEVersion(navigator.userAgent) < 9) {
width = document.body.clientWidth;
} else {
width = window.innerWidth;
}

这样做既不能保证数据准确,也难以维护.正确的做法是:

1
2
// 利用JS对不存在属性返回undefined的特性,使用短路运算符||
let width = window.innerWidth || document.body.clientWidth

screen

表示屏幕的信息

  • screen.width
  • screen.height
  • screen.colorDepth: 颜色位数,比如8,16,24

location

表示当前页面的URL信息,获取URL的个个部分

1
2
3
4
5
6
7
8
9
// 例如一个完整的URL
// http://www.example.com:8080/path/index.html?a=1&b=2#TOP
location.href; // 获取整个URL
location.protocol; // 'http'
location.host; // 'www.example.com'
location.port; // '8080'
location.pathname; // '/path/index.html'
location.search; // '?a=1&b=2'
location.hash; // 'TOP'

还有两个常用方法

location.reload(): 重新加载当前页面

localtion.assign():加载一个新页面(当前页跳转)

1
2
3
4
5
if (confirm('重新加载当前页' + location.href + '?')) {
location.reload();
} else {
location.assign('/'); // 设置一个新的URL地址
}

document

这个对象表示当前页面.

由于HTML在浏览器中以DOM形式表示为树状结构,document对象就是整个DOM树的根节点.

DOM(document object model 文档对象模型)

DOM是一个编程接口,它将HTML或XML文档表示为一个树形结构,其中每个节点代表文档的一部分.

这样可以让开发者通过编程方式来访问和修改文档的内容,结构和样式.

比如:

1
2
3
4
5
6
7
8
9
<html>
<head>
<title>我的页面</title>
</head>
<body>
<h1>欢迎</h1>
<p>这是一个段落</p>
</body>
</html>

在DOM中就会表示为:

1
2
3
4
5
6
7
8
9
10
document
└── html
├── head
│ └── title
│ └── "我的页面"
└── body
├── h1
│ └── "欢迎"
└── p
└── "这是一个段落"

比如documenttitle属性是从HTML的<title>xxx</title>读取的,但是可以动态改变

1
document.title = '好好学习,天天向上!';

要查找DOM树的某个节点,需要从document对象开始查找,最常用的查找的是根据IDTag Name.比如有如下html

1
2
3
4
5
6
7
8
<dl id="drink-menu" style="border:solid 1px #ccc;padding:6px;">
<dt>摩卡</dt>
<dd>热摩卡咖啡</dd>
<dt>酸奶</dt>
<dd>北京老酸奶</dd>
<dt>果汁</dt>
<dd>鲜榨苹果汁</dd>
</dl>
1
2
3
4
5
6
7
8
let menu = document.getElementById('drink-menu');
let drinks = document.getElementsByTagName('dt');
let s = '提供的饮料有:';

for (let i=0; i<drinks.length; i++) {
s = s + drinks[i].innerHTML + ','; // innerHTML是一个DOM属性,用于获取/设置DOM对象的HTML内容,类似的还有innerText,只获取文本内容
}
console.log(s);

document.cookie该属性可以获取当前页面的cookie.cookie是由服务器发送的K-V标识符.因为HTTP是无状态的,但是服务器要区分是哪个用户发过来的请求,就可以用cookie.当一个用户成功登录后,服务器发送一个cookie给浏览器,例如user=asfdf(加密的字符串),此后浏览器访问该网站时就会在请求头附上这个cookie,服务器根据cookie区别用户.另外cookie还可用于存储网站一些设置,比如语言等.

cookiesession

cookiesession
存储位置存储在客户端(浏览器)存储在服务端
安全性容易被窃取和篡改比较安全,因为数据在服务器上
存储容量通常限制4kb取决于服务端配置,一般可以存储更多数据
生命周期客户端可以设置过期时间以下情况会过期:
- 用户关闭浏览器
- 服务器端设置的过期时间到达
- 用户主动退出登录

一般

  • 敏感数据用Session
  • 用户偏好设置用Cookie
1
document.cookie; // 'v=123; remember=true; prefer=zh'

由于JavaScript可以获取到cookie,而用户的登录信息通常也会存在cookie,这会产生安全隐患.因为HTML页面可以引入第三方JS,也就是第三方可以通过JS获取用户的cookie.为了避免这种情况,服务器端应该将cookie设置为httpOnly.这样JS便不能获取cookie.

history

这个对象保存了浏览器的历史记录,通过它的back()fowward()方法,相当于用户点击”后退”或”前进”.

新手喜欢在登录页登录成功后调用history.back()返回登录前页面,这是一个错误的做法.

对使用AJAX动态加载的页面,如果希望页面更新时同时更新history对象,应当使用history.pushState()

1
2
3
4
// when AJAX is done:
let state = 'any-data';
let newUrl = '/ajax.html#singin';
history.pushState(state, '', newUrl);

操作DOM

  • 更新: 更新DOM节点的内容,相当于更新了DOM节点所表示HTML的内容
  • 遍历: 遍历该DOM节点下的子节点,以便进一步操作
  • 添加: 在该DOM节点下新增一个子节点,相当于动态增加了一个HTML节点
  • 删除: 将该节点从HTML删除,包括它的子节点也一并删除

遍历

  1. document.getElementById(): 由于ID在HTML文档中是唯一的,所以此方法可以定位唯一的DOM节点
  2. document.getElementByTagName()/document.getElementByClassName()总是返回一组DOM节点,可以先定位父节点,再慢慢定位
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 返回ID为'test'的节点:
let test = document.getElementById('test');

// 先定位ID为'test-table'的节点,再返回其内部所有tr节点:
let trs = document.getElementById('test-table').getElementsByTagName('tr');

// 先定位ID为'test-div'的节点,再返回其内部所有class包含red的节点:
let reds = document.getElementById('test-div').getElementsByClassName('red');

// 获取节点test下的所有直属子节点:
let cs = test.children;

// 获取节点test下第一个、最后一个子节点:
let first = test.firstElementChild;
let last = test.lastElementChild;
  1. querySelector()/querySelectorAll(): 需要了解selector语法
1
2
3
4
5
// 通过querySelector获取ID为q1的节点:
let q1 = document.querySelector('#q1');

// 通过querySelectorAll获取q1节点内的符合条件的所有节点:
let ps = q1.querySelectorAll('div.highlighted > p');

实际上DOM的节点有多种类型:

  • Element: 最常见的节点类型,代表html的各种标签
  • Comment: 就是注释
  • CDATA_SECTION: 主要用于XML,用于存储包含特殊字符的文本无需转义
  • Document: 就是整个HTML文档,也是DOM树的根节点

DOM节点称为Node,而我们上面操作的都是Element类型的Node

更新

修改DOM节点一般使用:

  • innerHtml: 可以设置HTML标签
1
2
3
4
5
6
7
// 获取<p id="p-id">...</p>
let p = document.getElementById('p-id');
// 设置文本为abc:
p.innerHTML = 'ABC'; // <p id="p-id">ABC</p>
// 设置HTML:
p.innerHTML = 'ABC <span style="color:red">RED</span> XYZ';
// <p>...</p>的内部结构已修改
  • innerText/textContent: 会对字符串自动编码,从而无法设置任何HTML标签
1
2
3
4
5
6
// 获取<p id="p-id">...</p>
let p = document.getElementById('p-id');
// 设置文本:
p.innerText = '<script>alert("Hi")</script>';
// HTML被自动编码,无法设置一个<script>节点:
// <p id="p-id">&lt;script&gt;alert("Hi")&lt;/script&gt;</p>

两者区别在于读取属性时,innerText不返回隐藏元素的文本,而textContent返回所有文本.

  • style: 对应所有CSS,可以通过这个属性获取或设置CSS
1
2
3
4
5
6
// 获取<p id="p-id">...</p>
let p = document.getElementById('p-id');
// 设置CSS:
p.style.color = '#ff0000';
p.style.fontSize = '20px'; // CSS中的命名是font-size,JS不允许这样的命名,所以改为驼峰命名法
p.style.paddingTop = '2em';

插入

如果获取到的DOM节点是空的,比如<div></div>,直接使用innerHtml = '<span>child</span>',就相当于插入了一个新的DOM节点.

但如果节点本身不为空,有两个办法插入:

  1. appendChild,把一个节点添加到父节点的最后一个子节点

    1
    2
    3
    4
    5
    6
    7
    <!-- 有这么一段html -->
    <p id="js">JavaScript</p>
    <div id="list">
    <p id="java">Java</p>
    <p id="python">Python</p>
    <p id="scheme">Scheme</p>
    </div>
    1
    2
    3
    4
    let 
    js = document.getElementById('js'),
    list = document.getElementById('list')
    list.appendChild(js);
    1
    2
    3
    4
    5
    6
    7
    <!-- HTML 会变成这样 -->
    <div id="list">
    <p id="java">Java</p>
    <p id="python">Python</p>
    <p id="scheme">Scheme</p>
    <p id="js">JavaScript</p>
    </div>

    如果要插入的节点已经存在当前的文档中,这个节点会从原来的位置删除,再插入.(id唯一)

    从零创建再插入:

    1
    2
    3
    4
    5
    6
    let
    list = document.getElementById('list'),
    haskell = document.createElement('p'); // 创建element类型的dom
    haskell.id = 'haskell'; // 给dom添加id
    haskell.innerText = 'Haskell'; // 给dom添加文本
    list.appendChild(haskell); // 插入

    动态添加DOM节点可以实现很多功能,比如

    1
    2
    3
    4
    let d = document.createElement('style');
    d.setAttribute('type', 'text/css');
    d.innerHTML = 'p { color: red }'; // 构建一个新的CSS元素
    document.getElementsByTag('head')[0].appendChild(d) // 插入

    上面动态创建了一个<style>节点,然后把它添加到<head>节点末尾,动态给document添加了新的CSS.

  2. inserBefore: 把子节点插入到指定位置,parentElemnet.insertBefore(newElement, referenceElement),新的子节点会插入到referenceElement之前

    1
    2
    3
    4
    5
    6
    7
    let
    list = document.getElementById('list'),
    ref = document.getElementById('python'),
    haskell = document.createElement('p');
    haskell.id = 'haskell';
    haskell.innerText = 'Haskell';
    list.insertBefore(haskell, ref);

    所以使用inserBefore的关键在于拿到准确的参考节点,很多时候需要循环一个父节点的所有子节点

    1
    2
    3
    4
    5
    6
    let
    i, c,
    list = document.getElementById('list');
    for (i=0; i<list.children.length; i++) {
    c = list.children[i]; // 拿到第i个子节点
    }
  3. 一个例子

    有如下的html

    1
    2
    3
    4
    5
    6
    7
    8
    <!-- HTML结构 -->
    <ol id="test-list">
    <li class="lang">Scheme</li>
    <li class="lang">JavaScript</li>
    <li class="lang">Python</li>
    <li class="lang">Ruby</li>
    <li class="lang">Haskell</li>
    </ol>

    需要把它们按字符串重新排序:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    let list = document.getElementById('test-list');
    let lis = list.children;
    let arr = [];
    let m = new Map();
    for(let e=0; e<lis.length; e++) {
    arr.push(lis[e].innerText);
    m.set(lis[e].innerText, lis[e]);
    }
    arr.sort()
    for (let a of arr) {
    list.appendChild(m.get(a)); // 如果该element已存在,则删除原有的再插入
    }

删除

首先获取该节点本身以及它的父节点,然后调用父节点的removeChild把自己删掉

1
2
3
4
5
6
7
// 拿到待删除节点
let self = document.getElementById('to-be-removed');
// 拿到它的父节点
let parent = self.parentElement;
// 删除自身
let removed = parent.removeChild(self);
removed === self; // true

注意:chidlren是一个只读属性,并且在它子节点变化时会实时更新

1
2
3
4
5
<!-- 例如有这样的html结构 -->
<div id="parent">
<p>First</p>
<p>Second</p>
</div>
1
2
3
let parent = document.getElementById('parent');
parent.removeChild(parent.children[0]);
parent.removeChild(parent.children[1]); // 报错

第二次remove报错是因为第一次remove后,children实时变化,索引[1]已经不存在

表单操作

表单本身也是DOM树.

表单的输入框,下拉框可以接收用户输入,所以JS可以获得用户输入的内容,或者对一个输入框设置新的内容.

HTML表单的输入控件主要有以下几种:

  • 文本框,对应的<input type="text">,用于输入文本;
  • 口令框,对应的<input type="password">,用于输入口令;
  • 单选框,对应的<input type="radio">,用于选择一项;
  • 复选框,对应的<input type="checkbox">,用于选择多项;
  • 下拉框,对应的<select>,用于选择一项;
  • 隐藏文本,对应的<input type="hidden">,用户不可见,但表单提交时会把隐藏文本发送到服务器。

获取值

对于text,input,password,hidden以及select

1
2
3
// <input type="text" id="email">
let input = document.getElementById('email');
input.value; // '用户输入的值'

但对于单选和复选,value返回的是HTML的预设值,也就是所有可选择项.要用checked判断是否用户已选择

1
2
3
4
5
6
7
8
// <label><input type="radio" name="weekday" id="monday" value="1"> Monday</label>
// <label><input type="radio" name="weekday" id="tuesday" value="2"> Tuesday</label>
let mon = document.getElementById('monday');
let tue = document.getElementById('tuesday');
mon.value; // '1'
tue.value; // '2'
mon.checked; // true或者false
tue.checked; // true或者false

设置值

textpasswordhidden以及select,直接设置value就可以

单选或复选就把checked设置为truefalse

1
2
3
// <input type="text" id="email">
let input = document.getElementById('email');
input.value = 'test@example.com'; // 文本框的内容已更新

HTML5控件

HTML5新增大量标准控件,常用的如date,datetime,datetime-local,color等,它们都是用<input>标签

不支持HTML5的浏览器无法识别新的控件,会把他们当作type='text'来显示.支持的将获得格式化的字符串.例如,type='date'类型的inputvalue将保证一个有效的YYYY-MM-DD格式的日期,或者空字符串.

提交表单

JS可以以两种方式处理表单的提交.

  1. 通过<form>元素的submit()方法提交一个表单,例如响应一个<button>click事件,在js代码中提交

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    < form id="test-form">
    <input type="text" name="test">
    <button type="button" onclick="doSumbmitForm()">
    Submit
    </button>
    </form>

    <script>
    function doSubmitForm() {
    let form = document.getElementById('test-form');
    // 这里可以修改form的input
    // 提交form
    form.submit()
    }
    </script>

    这种方式的缺点是扰乱了浏览器对form的正常提交.正常来说浏览器默认点击<button type='submit'>时提交表单,或者用户在最后一个输入框按回车键.因此就有了第二种方法

  2. 响应<form>本身的onsubmit事件.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <!-- HTML -->
    <form id="test-form" onsubmit="return checkForm()">
    <input type="text" name="test">
    <button type="submit">Submit</button>
    </form>

    <script>
    function checkForm() {
    let form = document.getElementById('test-form');
    // 可以在此修改form的input...
    // 继续下一步:
    return true; // 返回true来告诉浏览器继续提交,如果返回false,浏览器将不会继续提交,可用于检查用户输入出错的场景
    }
    </script>

<input type="hidden">的妙用:

很多时候用户输入用户名和口令时,出于安全考虑不会传递明文,会把它转为MD5再传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- HTML -->
<form id="login-form" method="post" onsubmit="return checkForm()">
<input type="text" id="username" name="username">
<input type="password" id="password" name="password">
<button type="submit">Submit</button>
</form>

<script>
function checkForm() {
let pwd = document.getElementById('password');
// 把用户输入的明文变为MD5:
pwd.value = toMD5(pwd.value);
// 继续下一步:
return true;
}
</script>

这个做法本身没什么问题,但是当用户输入了密码提交时,密码框会突然从几个*变成32个*(MD5有32个字符)

如果不想有这个效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- HTML -->
<form id="login-form" method="post" onsubmit="return checkForm()">
<input type="text" id="username" name="username">
<input type="password" id="input-password"> <!-- 用于给用户输入的password 没有name属性, 没有name属性的input不会被提交-->
<input type="hidden" id="md5-password" name="password">
<button type="submit">Submit</button>
</form>

<script>
function checkForm() {
let input_pwd = document.getElementById('input-password');
let md5_pwd = document.getElementById('md5-password');
// 把用户输入的明文变为MD5:
md5_pwd.value = toMD5(input_pwd.value);
// 继续下一步:
return true;
}
</script>

文件操作

HTML表单中,可以上传文件的唯一控件就是<input type='file'>

**注意:**当一个表单包含<input type='file'>时:

  • enctype必须为multipart/form-data
  • method必须为post
  • 这样浏览器才能正确编码以multipart/form-data格式发送表单数据

出于安全考虑,浏览器只允许用户点击<input type='file'>来选择本地文件,用JS给<input type='file'>de value赋值是没有任何效果的.当用户上传了某个文件后,JS也无法获得文件的真实路径.

通常上传的文件都是由后台服务器处理.JS可以在提交表单时对文件扩展名做检查,凡是用户上传无效的格式

1
2
3
4
5
6
let f = document.getElementById('test-file-upload');
let filename = f.value;
if (!filename || !(filename.endsWith('.jpg') || filename.endsWith('.pbg') || filename.endsWith('.gif'))) {
alert('Can only upload image file.');
return false;
}

File API

由于JS对用户上传的文件操作非常优先,尤其无法读取文件内容,使得很多需要操作文件的网页不得不用Flash这样的第三方插件来实现.

随着HTML5的普及,新增的File API允许JS读取文件内容,获得更多信息.

HTML5的File API提供了FileFileReader两个主要对象,可以获得文件信息并读取文件.

下面的例子演示如何读取用户上传的图片并在一个div中显示.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
let
fileInput = document.getElementById('test-image-file'),
info = document.getElementById('test-file-info'),
preview = document.getElementById('test-image-preview');
// 监听change事件:
fileInput.addEventListener('change', function () {
// 清除背景图片:
preview.style.backgroundImage = '';
// 检查文件是否选择:
if (!fileInput.value) {
info.innerHTML = '没有选择文件';
return;
}
// 获取File引用:
let file = fileInput.files[0];
// 获取File信息:
info.innerHTML = '文件: ' + file.name + '<br>' +
'大小: ' + file.size + '<br>' +
'修改: ' + file.lastModified;
if (file.type !== 'image/jpeg' && file.type !== 'image/png' && file.type !== 'image/gif') {
alert('不是有效的图片文件!');
return;
}
// 读取文件:
let reader = new FileReader(); // 通过FileReader对象读取文件内容
reader.onload = function(e) {
let
data = e.target.result; // 'data:image/jpeg;base64,/9j/4AAQSk...(base64编码)...'
preview.style.backgroundImage = 'url(' + data + ')'; // 把读取的图片内容放入预览框
};
// 以DataURL的形式读取文件:
reader.readAsDataURL(file);
});

DataURL形式读取到的文件是一个字符串,类似data:image/jpeg;base64,/9j/4AAQSk...(base64编码)...,常用于设置图像.如果需要服务端处理,把字符串base64,后面的字符发送给服务端,并用base64解码就可以得到原始文件的二进制内容.

回调

上面的代码还演示了JS的一个重要特性就是单线程执行模式.

浏览器的JS执行引擎在执行JS代码时,总是单线程模式执行.任何时候JS都不可能同时有多于一个线程执行.

JS中的多任务是通过异步来实现的,比如上面的

1
reader.readAsDataURL(file);

就会发起一个异步操作来读取文件内容.因为是异步操作,所以我们在JS中就不知道什么时候操作结束,因此需要先设置一个回调函数.

1
2
3
4
// reader.onload表示文件读取完成的事件
reader.onload = function(e) {
// 当文件读取完成,自动调用此函数
};

当文件读取完成后,JavaScript引擎将自动调用我们设置的回调函数。执行回调函数时,文件已经读取完毕,所以我们可以在回调函数内部安全地获得文件内容。

AJAX

AJAX是一个缩写: Asynchronous JavaScript and XML,意思就是用JavaScript执行异步网络请求.

如果仔细观察一个Form的提交,你就会发现,一旦用户点击“Submit”按钮,表单开始提交,浏览器就会刷新页面,然后在新页面里告诉你操作是成功了还是失败了。如果不幸由于网络太慢或者其他原因,就会得到一个404页面。

这就是Web的运作原理:一次HTTP请求对应一个页面

如果要让用户留在当前页面中,同时发出新的HTTP请求,就必须用JavaScript发送这个新请求,接收到数据后,再用JavaScript更新页面,这样一来,用户就感觉自己仍然停留在当前页面,但是数据却可以不断地更新。

最早大规模使用AJAX的就是Gmail,Gmail的页面在首次加载后,剩下的所有数据都依赖于AJAX来更新。

用JavaScript写一个完整的AJAX代码并不复杂,但是需要注意:AJAX请求是异步执行的,也就是说,要通过回调函数获得响应。

现在浏览器写AJAX主要依靠XMLHttpRequest对象,另外还提供了原生支持的Fetch API以Promise(下面会说到)方式提供.使用Fetch API发送HTTP请求代码如下:

1
2
3
4
5
6
7
8
9
10
11
async function get(url) {
let resp = await fetch(url);
let result = await resp.text();
return result;
}

// 发送异步请求
get('./content.html').then(data => {
let textarea = document.getElementById('fetch-response-text');
textarea.value = data;
});

使用Fetch API 配合async写法,代码更简单.

Fetch API详细用法可以参考MDN文档

安全限制

上面的代码使用./content.html相对路径.如果你把它改为https://baidu.com,在运行就会报错.在chrome的浏览器控制台还可以看到错误信息.

这就是浏览器的同源策略:

默认情况下,JS发送AJAX请求时,URL域名必须和当前页面完全一致.

完全一致的意思是:

  • 域名:www.example.comexample.com是两个域名
  • 协议: httphttps是不同协议
  • 端口号也要相同

那如果想通过JS去请求外域可以怎么做呢?

  1. 通过Flash插件发送HTTP请求,这种方式可以绕过安全限制,但是Flash已经淘汰了.

  2. 通过同源域名下假设代理服务器转发,JS把请求发送到代理服务器

    1
    '/proxy?url=https://www.sina.com.cn'
  3. 称为JSONP,有个限制,只能用GET请求,且要求返回JS.因为浏览器允许跨域引用JS资源.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <html>
    <head>
    <script src="http://example.com/abc.js"></script>
    ...
    </head>
    <body>
    ...
    </body>
    </html>

    JSNOP通常以函数形式返回,例如但会一个foo

    1
    foo('data');

    这样一来,我们如果在页面先准备好foo()函数,然后给页面动态加一个<script>节点,相当于动态读取外域JS资源,然后就等着接收回调.

CORS

支持HTML5的现代浏览器有新的跨域策略可以使用: CORS

全称Cross-Origin Resource Sharing,是HTML5规范定义的如何跨域访问资源

Origin表示本域,也就是浏览器当前页面的域。当JavaScript向外域(如sina.com)发起请求后,浏览器收到响应后,首先检查Access-Control-Allow-Origin是否包含本域,如果是,则此次跨域请求成功,如果不是,则请求失败,JavaScript将无法获取到响应的任何数据。

1
2
3
4
5
6
7
8
9
10
         GET /res/abc.data
Host: sina.com
┌──────┐ Origin: http://my.com ┌────────┐
│my.com│───────────────────────────────────────────▶│sina.com│
│ │◀───────────────────────────────────────────│ │
└──────┘ HTTP/1.1 200 OK └────────┘
Access-Control-Allow-Origin: http://my.com
Content-Type: text/xml

<xml data...>

只要返回的响应头里的Access-Control-Allow-Origin包含本域http://my.com或者*,本次请求就能成功.

也就是说决定权在对方手上,就看它愿不愿意返回一个正确的Access-Control-Allow-Origin.

目前新的浏览器都支持CORS,也就是除了JS和CSS外,所有外域资源都要验证CORS.比如你引用某个第三方CDN字体文件

1
2
3
4
@font-face {
font-family: 'FontAwesome';
src: url('http://cdn.com/fonts/fontawesome.ttf') format('truetype');
}

上面的这些跨域请求,称为简单请求,包括:

  • GET
  • HEAD
  • POST: content-type类型限制为
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain
    • 并且不能出现任何自定义头

而对于PUT和其他类型如application/jsonPOST请求,在发送AJAX之前,浏览器会先发送一个OPTIONS请求(称为preflighted请求)到这个URL上,询问目标服务器是否接收

1
2
3
4
OPTIONS /path/to/resource HTTP/1.1
Host: bar.com
Origin: http://my.com
Access-Control-Request-Method: POST

服务器必须响应并明确指出明确的允许的Methods

1
2
3
4
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://my.com
Access-Control-Allow-Methods: POST, GET, PUT, OPTIONS
Access-Control-Max-Age: 86400

浏览器确认服务器响应的Access-Control-Allow-Methods头确实包含将要发送的AJAX请求的Method,才会继续发送AJAX,否则,抛出一个错误。

由于以POSTPUT方式传送JSON格式的数据在REST中很常见,所以要跨域正确处理POSTPUT请求,服务器端必须正确响应OPTIONS请求。

关于CROS更多可详阅MDN文档W3C文档

Promise

JS中,所有代码都是但线程执行,着导致JS的所有网络操作,浏览器事件,都必须是异步执行.异步执行可以用回调函数实现.

1
2
3
4
5
6
function callback() {
console.log('Done');
}
console.log('before setTimeout()');
setTimeout(callback, 1000); // 1秒后调用callback函数
console.log('after setTimeout()');

setTimeout()做了两件事:

  • 注册了一个定时器
  • 告诉JS引擎在1000毫秒后把callback函数放入任务队列

这也称为定时器回调,类似的还有事件回调比如上面的读文件和提交表单

浏览器console输入如下:

1
2
3
4
before setTimeout()
after setTimeout()
(等待1秒后)
Done

AJAX异步操作: 传统XMLHttpRequest方式定义

1
2
3
4
5
6
7
8
9
request.onreadystatechange = function() {
if(requenst.readyState === 4) { // 4表示请求完成
if(request.status === 200) {
return success(request.responseText); // 调用成功回调
} else {
return fail(request.status); // 调用失败回调
}
}
}

把回调函数success(request.responseText)fail(request.status)写到一个AJAX操作里,这种方式已经很少使用.不好看也不利于代码复用.

前置概念

  • 同步: 代码按顺序一行一行执行,上一行代码执行完毕才能执行下一行
  • 异步: 代码的执行不需要等待上一个任务的完成.例如网络请求,定时器等.异步操作不会阻塞主线程执行.

JS是单线程语言,意味着它一次只能执行一个任务.为了处理耗时的异步操作而不阻塞主线程,JS使用事件循环和回调函数机制.Promise是建立在这个机制之上的更高级的异步处理方案.

基本概念

Promise有三种状态:

  • Pending: 初始状态,异步操作正在进行中
  • fulfilled: 异步操作成功完成
  • rejected: 异步操作失败

Promise对象提供then()catch()方法来处理异步操作的结果或错误.

then() 和 catch() 是 Promise 对象上用于处理异步操作结果的关键方法。它们允许你指定回调函数,分别在 Promise 完成(fulfilled)拒绝(rejected)时执行

then()接收一个或两个参数:

  • 第一个参数(onFulfilled): 一个回调函数,当promise完成(fulfilled)时调用.这个回调函数会接收Promise的完成值作为参数
  • 第二个参数(onRejected)(可选): 一个回调函数,当Promise拒绝(rejected)时调用.这个回调函数会接收Promise的拒绝原因(通常是一个Error对象)作为参数

catch() 方法是then(null, onRejected) 的简写形式,它专门用于处理 Promise 的拒绝(rejected)状态。它接受一个回调函数作为参数,当 Promise 拒绝时调用。


resolve()rejected()是JS Promise API提供的一个内部函数.当你使用new Promise()构造函数创建一个Promise,Promise会将resolvereject两个函数作为参数传递给你的执行器函数

1
2
3
new Promise((resolve, reject) => { 
// ...你的异步操作代码...
});
  • resolve(): 一个函数,用于将Promise状态从pending改为fulfilled(已完成).你需要在异步成功完成后调用resolve(),并将操作的结果作为参数传递给它.例如resolve('操作成功')resolve({data: someData})
  • reject(): 一个函数,用于将Promise的状态从pending改为rejected(已拒绝).你需要在异步操作失败时调用reject(),并将错误原因(一般是一个Error对象)作为参数传递给它.例如:reject(new Error("操作失败")).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 一个简单的Promise例子
// 创建一个promise对象,传入两个函数,一个是成功时执行,一个失败时执行
let promise = new Promise((resolve, reject) => {
// setTimeout(callback, delay) 延时执行callback函数,用来模拟一个异步操作
setTimeout(() => {
let success = true;
if (success) {
resolve("操作成功!");
} else {
reject("操作失败!");
}
}, 1000);
});

// 使用then()处理成功的结果
promise.then(
(result) => {
console.log(result);
},
(error) => {
console.error(error);
}
);

// 使用更简洁的链式调用和catch()处理错误
promise.then(
result => {
console.log(result);
}
).catch(
error => {
console.log(error);
}
)

链式调用

因为then()方法返回一个新的Promise对象,因此可以实现链式调用,异步操作更加清晰和易于维护

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function asyncOperation1() {
return new Promise(
(resolve) => {
// 箭头函数的函数体只有一行时,可以省略return和{},下面的箭头函数等价于: () => { return resolve("操作1完成") }
setTimeout(() => resolve("操作1完成"), 1000);
}
);
}

function asyncOperation2(result1) {
return new Promise(
(resolve) => {
setTimeout(() => resolve(result1 + ', 操作2完成'), 1500);
}
);
}

asyncOperation1()
/*
回调函数内部调用了"asyncOperation2()",并将
*/
.then((result1) => asyncOperation2(result1))
.then((result2) => console.log(result2))
.catch((error) => console.error(error));
  • asyncOperation1(): 调用asyncOperation1()函数,这个函数返回一个Promise,这个Promise在1秒后resolve为操作1完成.
  • .then((result1) => asyncOperation2(result1)): 第一个.then()方法被调用,它的回调函数(result1) => asyncOperation2(result1)会在asyncOperation1()返回Promise fulfilled后执行.
    • result1: asyncOperation1()的Promise resolve值(操作1完成)会被作为参数传递给第一个.then()的回调函数,这里的result1只是自定义的一个变量名,它会被自动赋值,你把它改为其他名字一样的作用.
    • asyncOperation2(result1): 回调函数内部调用了asyncOperation2函数.并将result1(也就是”操作1完成”)作为参数传递给它.asyncOperation2也会返回一个Promise,这个Promise在1.5秒后resolve为result1 + ', 操作2完成',也就是”操作1完成,操作2完成”
  • .then((result2) => console.log(result2)): 第二个.then()方法被调用.它的回调函数(result2) => console.log(result2)会在第一个.then()返回新的Promise fulfilled后执行.
    • result2: asyncOperation2()返回的Promise resolve的值(“操作1完成,操作2完成”)会被作为参数传给第二个.then()的回调函数,赋值给result2,同样,名字随意.
    • console.log(result2): 打印result2的值
  • .catch((error) => console.error(error))): 用于捕获Promise链中任何一个Promise出现rejected状态的情况.

总结: 理解Promise链的关键在于理解:

  1. .then()方法会获取上一个Promise对象的resolve返回,并把它作为参数传递给.then()回调函数
  2. 如果上一个Promise内reject()了,那么.then()的第一个回调函数就不会被执行,代码跳到链上的下一个.catch()或者直接抛出错误(如果没有.catch())
  3. .then() 方法不仅是处理 Promise 完成后的结果,更是构建 Promise 链、实现异步操作顺序执行的关键。 它通过接收前一个 Promise 的结果作为输入,来实现异步操作之间的依赖关系,让后续操作建立在前面的操作结果之上

但是其实可以看到Promise链理解起来十分复杂,写起来也一样复杂,大多数情况下,要使用async/await,这是针对Promise的语法糖,它使得异步代码看起来更像同步代码,从而大大简化代码的复杂度喝可读性.只有在一些非常特殊的情况下(例如,需要非常精细地控制 Promise 的执行顺序,或者与一些老旧的库进行交互),你才可能需要编写更复杂的 Promise 链。 而大多数情况下,应该尽量避免复杂的 Promise 链,而选择更清晰和易于维护的方法。

async

在JS中,async关键字用于声明一个异步函数.异步函数总是返回一个Promise对象,即使函数体没有显示地返回一个Promise.

特点:

  • 总是返回一个Promise: 一个 async 函数无论是否显式返回一个 Promise,都会隐式地返回一个 Promise。如果 async 函数返回一个值,这个值会被 Promise.resolve() 包装成一个 fulfilled 的 Promise;如果 async 函数抛出一个异常,则返回一个 rejected 的 Promise。
  • await关键字: async关键字内部可以使用await关键字,暂停函数的执行,知道一个Promise完成(fulfilled或rejected).这使得异步代码更容易阅读和编写,看起来更像同步代码.
  • 清晰的错误处理: 使用try...catch直接不过异步操作中可能发生的错误.
  • 简化异步操作: async/await使得处理多个异步操作变得更加简单和易于理解,避免promise链的嵌套和复杂.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function someAsyncOperation1() {
return new Promise((resolve) => {
setTimeout(() => resolve("Operation 1 completed"), 1000);
});
}

function someAsyncOperation2(data) {
return new Promise((resolve) => {
setTimeout(() => resolve(data + " - Operation 2 completed"), 1500);
});
}

async function myAsyncFunction() {
try {
const result1 = await someAsyncOperation1(); // 等待someAsyncOperation1完成
const result2 = await someAsyncOperation2(result1); // 等待someAsyncOperation2完成
return result2; // 返回一个promise,值为result2
} catch (error) {
console.error("Error:", error);
return null; // 或者抛出错误 throw error
}
}

myAsyncFunction()
.then((result) => console.log("final result:", result))
.catch((error) => console.log("Caught error:", error));
  • myAsyncFunction 是一个 async 函数。
  • await 关键字用于等待 someAsyncOperation1 和 someAsyncOperation2 异步操作完成。
  • try…catch 块用于处理潜在的错误。
  • 函数最终返回一个 Promise。

async函数的本质是对Promise的语法糖.它使得异步操作得编写简洁易懂,底层仍然依赖于Promise.

所以你可以看到,使用async/await来编写异步代码更加便捷易懂,但是它返回的永远是一个Promise对象.所以依旧需要thencatch来处理成功的结果/可能发生的错误

错误返回

1
2
3
4
5
6
7
...
} catch (error) {
console.error("Error:", error);
return null; // 或者抛出错误 throw error;
throw error; // 这两行代码二选一
}
...

上面的错误处理,return null;throw error;是两种截然不同的操作.

  • return null

    只是简单地返回一个null,不会中断Promise链的执行,也不会触发.catch(),return null只是改变了Promise的resolve值,但Promise的状态仍然是fulfilled

  • throw error;

    则会将错误抛出,把promise的状态改为rejected,从而触发.catch()方法.这个错误也能被调用方用try...catch来捕获.触发了throw error会中断Promise链. 更准确地说,它会将 Promise 的状态设置为 rejected,并且会停止后续 .then() 方法中 第一个参数(onFulfilled) 回调函数的执行。 错误会沿着 Promise 链向上“冒泡”,直到遇到一个 .catch() 方法来处理它,或者最终导致程序报错(如果没有 .catch() 处理)。

下面是一个用try...catch捕获threw error的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
async function myAsyncFunction() {
try {
const result1 = await someAsyncOperation1();
const result2 = await someAsyncOperation2(result1);
return result2;
} catch (error) {
console.error("Error in myAsyncFunction:", error); //记录错误信息
throw error; // 将错误抛出到调用者
}
}

async function main() {
try {
const result = await myAsyncFunction();
console.log("Result:", result);
} catch (error) {
console.error("Error caught in main function:", error); // 在这里处理抛出的错误
}
}

main();

从上面的例子也能看出,async定义的异步函数可以通过另一个异步函数(main)来调用,并使用try...catch来捕获错误.从而避免了.then().catch()的使用.这是一种更优雅更易的方式.但是要谨记,这些都只是语法糖,底层依旧是Promise对象.

Canvas

是HTML5的一个组件,提供一个可以用来绘制2D图形的位图.它本质上是一个可以被JS代码操作的画布.你可以用JS在它上面绘制各种图形,图像和文字.

主要用途:

  • 图形绘制
  • 图像处理: 缩放,选装,裁剪,滤镜等
  • 动画制作: 通过不停更新canvas的内容创建动画效果
  • 游戏开发: 是很多2D游戏的开发基础
  • 数据可视化

局限性:

  • 性能: 非常复杂的图形和动画,推荐使用WebGL(一种基于canvas的3D图形库)
  • DOM操作: canvas本身不是DOM元素,所以不支持DOM API来操作.
  • 矢量图: 不支持矢量图,矢量图缩放不会失真.所以Canvas绘制的图形缩放会失真.SVG是处理矢量图更好的选择.

JavaScript-浏览器
http://example.com/2024/11/15/js-browser/
作者
Peter Pan
发布于
2024年11月15日
许可协议