文将通过简单的术语和真实世界的例子解释 JavaScript 中 this 及其用途,并告诉你写出好的代码为何如此重要。
我看到许多文章在介绍 JavaScript 的 this 时都会假设你学过某种面向对象的编程语言,比如 Java、C++ 或 Python 等。但这篇文章面向的读者是那些不知道 this 是什么的人。我尽量不用任何术语来解释 this 是什么,以及 this 的用法。
也许你一直不敢解开 this 的秘密,因为它看起来挺奇怪也挺吓人的。或许你只在 StackOverflow 说你需要用它的时候(比如在 React 里实现某个功能)才会使用。
在深入介绍 this 之前,我们首先需要理解函数式编程和面向对象编程之间的区别。
你可能不知道,JavaScript 同时拥有面向对象和函数式的结构,所以你可以自己选择用哪种风格,或者两者都用。
我在很早以前使用 JavaScript 时就喜欢函数式编程,而且会像躲避瘟疫一样避开面向对象编程,因为我不理解面向对象中的关键字,比如 this。我不知道为什么要用 this。似乎没有它我也可以做好所有的工作。
而且我是对的。
在某种意义上 。也许你可以只专注于一种结构并且完全忽略另一种,但这样你只能是一个 JavaScript 开发者。为了解释函数式和面向对象之间的区别,下面我们通过一个数组来举例说明,数组的内容是 Facebook 的好友列表。
假设你要做一个 Web 应用,当用户使用 Facebook 登录你的 Web 应用时,需要显示他们的 Facebook 的好友信息。你需要访问 Facebook 并获得用户的好友数据。这些数据可能是 firstName、lastName、username、numFriends、friendData、birthday 和 lastTenPosts 等信息。
const data = [ { firstName: 'Bob', lastName: 'Ross', username: 'bob.ross', numFriends: 125, birthday: '2/23/1985', lastTenPosts: ['What a nice day', 'I love Kanye West', ...], }, ... ]
假设上述数据是你通过 Facebook API 获得的。现在需要将其转换成方便你的项目使用的格式。我们假设你想显示的好友信息如下:
函数式的方式就是将整个数组或者数组中的某个元素传递给某个函数,然后返回你需要的信息:
const fullNames = getFullNames(data) // ['Ross, Bob', 'Smith, Joanna', ...]
首先我们有 Facebook API 返回的原始数据。为了将其转换成需要的格式,首先要将数据传递给一个函数,函数的输出是(或者包含)经过修改的数据,这些数据可以在应用中向用户展示。
我们可以用类似的方法获得随机三篇文章,并且计算距离好友生日的天数。
函数式的方式是:将原始数据传递给一个函数或者多个函数,获得对你的项目有用的数据格式。
对于编程初学者和 JavaScript 初学者,面向对象的概念可能有点难以理解。其思想是,我们要将每个好友变成一个对象,这个对象能够生成你一切开发者需要的东西。
你可以创建一个对象,这个对象对应于某个好友,它有 fullName 属性,还有两个函数 getThreeRandomPosts 和 getDaysUntilBirthday。
function initializeFriend(data) { return { fullName: `${data.firstName} ${data.lastName}`, getThreeRandomPosts: function() { // get three random posts from data.lastTenPosts }, getDaysUntilBirthday: function() { // use data.birthday to get the num days until birthday } }; } const objectFriends = data.map(initializeFriend) objectFriends[0].getThreeRandomPosts() // Gets three of Bob Ross's posts
面向对象的方式就是为数据创建对象,每个对象都有自己的状态,并且包含必要的信息,能够生成需要的数据。
你也许从来没想过要写上面的 initializeFriend 代码,而且你也许认为,这种代码可能会很有用。但你也注意到,这并不是真正的面向对象。
其原因就是,上面例子中的 getThreeRandomPosts 或 getdaysUntilBirtyday 能够正常工作的原因其实是闭包。因为使用了闭包,它们在 initializeFriend 返回之后依然能访问 data。关于闭包的更多信息可以看看这篇文章:作用域和闭包(https://github.com/getify/You-Dont-Know-JS/blob/master/scope%20%26%20closures/ch5.md)。
还有一个方法该怎么处理?我们假设这个方法叫做 greeting。注意方法(与 JavaScript 的对象有关的方法)其实只是一个属性,只不过属性值是函数而已。我们想在 greeting 中实现以下功能:
function initializeFriend(data) { return { fullName: `${data.firstName} ${data.lastName}`, getThreeRandomPosts: function() { // get three random posts from data.lastTenPosts }, getDaysUntilBirthday: function() { // use data.birthday to get the num days until birthday }, greeting: function() { return `Hello, this is ${fullName}'s data!` } }; }
这样能正常工作吗?
不能!
我们新建的对象能够访问 initializeFriend 中的一切变量,但不能访问这个对象本身的属性或方法。当然你会问,
难道不能在 greeting 中直接用 data.firstName 和 data.lastName 吗?
当然可以。但要是想在 greeting 中加入距离好友生日的天数怎么办?我们最好还是有办法在 greeting 中调用 getDaysUntilBirthday。
这时轮到 this 出场了!
this 在不同的环境中可以指代不同的东西。默认的全局环境中 this 指代的是全局对象(在浏览器中 this 是 window 对象),这没什么太大的用途。而在 this 的规则中具有实用性的是这一条:
如果在对象的方法中使用 this,而该方法在该对象的上下文中调用,那么 this 指代该对象本身。
你会说“在该对象的上下文中调用”……是啥意思?
别着急,我们一会儿就说。
所以,如果我们想从 greeting 中调用 getDaysUntilBirtyday 我们只需要写 this.getDaysUntilBirthday,因为此时的 this 就是对象本身。
附注:不要在全局作用域的普通函数或另一个函数的作用域中使用 this!this 是个面向对象的东西,它只在对象的上下文(或类的上下文)中有意义。
我们利用 this 来重写 initializeFriend:
function initializeFriend(data) { return { lastTenPosts: data.lastTenPosts, birthday: data.birthday, fullName: `${data.firstName} ${data.lastName}`, getThreeRandomPosts: function() { // get three random posts from this.lastTenPosts }, getDaysUntilBirthday: function() { // use this.birthday to get the num days until birthday }, greeting: function() { const numDays = this.getDaysUntilBirthday() return `Hello, this is ${this.fullName}'s data! It is ${numDays} until ${this.fullName}'s birthday!` } }; }
现在,在 initializeFriend 执行结束后,该对象需要的一切都位于对象本身的作用域之内了。我们的方法不需要再依赖于闭包,它们只会用到对象本身包含的信息。
好吧,这是 this 的用法之一,但你说过 this 在不同的上下文中有不同的含义。那是什么意思?为什么不一定会指向对象自己?
有时候,你需要将 this 指向某个特定的东西。一种情况就是事件处理函数。比如我们希望在用户点击好友时打开好友的 Facebook 首页。我们会给对象添加下面的 onClick 方法:
function initializeFriend(data) { return { lastTenPosts: data.lastTenPosts, birthday: data.birthday, username: data.username, fullName: `${data.firstName} ${data.lastName}`, getThreeRandomPosts: function() { // get three random posts from this.lastTenPosts }, getDaysUntilBirthday: function() { // use this.birthday to get the num days until birthday }, greeting: function() { const numDays = this.getDaysUntilBirthday() return `Hello, this is ${this.fullName}'s data! It is ${numDays} until ${this.fullName}'s birthday!` }, onFriendClick: function() { window.open(`https://facebook.com/${this.username}`) } }; }
注意我们在对象中添加了 username 属性,这样 onFriendClick 就能访问它,从而在新窗口中打开该好友的 Facebook 首页。现在只需要编写 HTML:
<button id="Bob_Ross"> <!-- A bunch of info associated with Bob Ross --> </button>
还有 JavaScript:
const bobRossObj = initializeFriend(data[0]) const bobRossDOMEl = document.getElementById('Bob_Ross') bobRossDOMEl.addEventListener("onclick", bobRossObj.onFriendClick)
在上述代码中,我们给 Bob Ross 创建了一个对象。然后我们拿到了 Bob Ross 对应的 DOM 元素。然后执行 onFriendClick 方法来打开 Bob 的 Facebook 主页。似乎没问题,对吧?
有问题!
哪里出错了?
注意我们调用 onclick 处理程序的代码是 bobRossObj.onFriendClick。看到问题了吗?要是写成这样的话能看出来吗?
bobRossDOMEl.addEventListener("onclick", function() { window.open(`https://facebook.com/${this.username}`) })
现在看到问题了吗?如果把事件处理程序写成 bobRossObj.onFriendClick,实际上是把 bobRossObj.onFriendClick 上保存的函数拿出来,然后作为参数传递。它不再“依附”在 bobRossObj 上,也就是说,this 不再指向 bobRossObj。它实际指向全局对象,也就是说 this.username 不存在。似乎我们没什么办法了。
轮到绑定上场了!
我们需要明确地将 this 绑定到 bobRossObj 上。我们可以通过 bind 实现:
const bobRossObj = initializeFriend(data[0]) const bobRossDOMEl = document.getElementById('Bob_Ross') bobRossObj.onFriendClick = bobRossObj.onFriendClick.bind(bobRossObj) bobRossDOMEl.addEventListener("onclick", bobRossObj.onFriendClick)
之前,this 是按照默认的规则设置的。但使用 bind 之后,我们明确地将 bobRossObj.onFriendClick 中的 this 的值设置为 bobRossObj 对象本身。
到此为止,我们看到了为什么要使用 this,以及为什么要明确地绑定 this。最后我们来介绍一下,this 实际上是箭头函数。
你也许注意到了箭头函数最近很流行。人们喜欢箭头函数,因为很简洁、很优雅。而且你还知道箭头函数和普通函数有点区别,尽管不太清楚具体区别是什么。
简而言之,两者的区别在于:
在定义箭头函数时,不管 this 指向谁,箭头函数内部的 this 永远指向同一个东西。
嗯……这貌似没什么用……似乎跟普通函数的行为一样啊?
我们通过 initializeFriend 举例说明。假设我们想添加一个名为 greeting 的函数:
function initializeFriend(data) { return { lastTenPosts: data.lastTenPosts, birthday: data.birthday, username: data.username, fullName: `${data.firstName} ${data.lastName}`, getThreeRandomPosts: function() { // get three random posts from this.lastTenPosts }, getDaysUntilBirthday: function() { // use this.birthday to get the num days until birthday }, greeting: function() { function getLastPost() { return this.lastTenPosts[0] } const lastPost = getLastPost() return `Hello, this is ${this.fullName}'s data! ${this.fullName}'s last post was ${lastPost}.` }, onFriendClick: function() { window.open(`https://facebook.com/${this.username}`) } }; }
这样能运行吗?如果不能,怎样修改才能运行?
答案是不能。因为 getLastPost 没有在对象的上下文中调用,因此getLastPost 中的 this 按照默认规则指向了全局对象。
你说没有“在对象的上下文中调用”……难道它不是从 initializeFriend 返回的内部调用的吗?如果这还不叫“在对象的上下文中调用”,那我就不知道什么才算了。
我知道“在对象的上下文中调用”这个术语很模糊。也许,判断函数是否“在对象的上下文中调用”的好方法就是检查一遍函数的调用过程,看看是否有个对象“依附”到了函数上。
我们来检查下执行 bobRossObj.onFriendClick() 时的情况。“给我对象 bobRossObj,找到其中的 onFriendClick 然后调用该属性对应的函数”。
我们同样检查下执行 getLastPost() 时的情况。“给我名为 getLastPost 的函数然后执行。”看到了吗?我们根本没有提到对象。
好了,这里有个难题来测试你的理解程度。假设有个函数名为 functionCaller,它的功能就是调用一个函数:
functionCaller(fn) { fn() }
如果调用 functionCaller(bobRossObj.onFriendClick) 会怎样?你会认为 onFriendClick 是“在对象的上下文中调用”的吗?this.username有定义吗?
我们来检查一遍:“给我 bobRosObj 对象然后查找其属性 onFriendClick。取出其中的值(这个值碰巧是个函数),然后将它传递给 functionCaller,取名为 fn。然后,执行名为 fn 的函数。”注意该函数在调用之前已经从 bobRossObj 对象上“脱离”了,因此并不是“在对象的上下文中调用”的,所以 this.username 没有定义。
这时可以用箭头函数解决这个问题:
function initializeFriend(data) { return { lastTenPosts: data.lastTenPosts, birthday: data.birthday, username: data.username, fullName: `${data.firstName} ${data.lastName}`, getThreeRandomPosts: function() { // get three random posts from this.lastTenPosts }, getDaysUntilBirthday: function() { // use this.birthday to get the num days until birthday }, greeting: function() { const getLastPost = () => { return this.lastTenPosts[0] } const lastPost = getLastPost() return `Hello, this is ${this.fullName}'s data! ${this.fullName}'s last post was ${lastPost}.` }, onFriendClick: function() { window.open(`https://facebook.com/${this.username}`) } }; }
上述代码的规则是:
在定义箭头函数时,不管 this 指向谁,箭头函数内部的 this 永远指向同一个东西。
箭头函数是在 greeting 中定义的。我们知道,在 greeting 内部的 this 指向对象本身。因此,箭头函数内部的 this 也指向对象本身,这正是我们需要的结果。
this 有时很不好理解,但它对于开发 JavaScript 应用非常有用。本文当然没能介绍 this 的所有方面。一些没有涉及到的话题包括:
我建议你首先问问自己在这些情况下的 this,然后在浏览器中执行代码来检验你的结果。
想学习更多关 于this 的内容,可参考《你不知道的 JS:this 和对象原型》:
如果你想测试自己的知识,可参考《你不知道的JS练习:this和对象原型》:
原文:https://medium.freecodecamp.org/a-deep-dive-into-this-in-javascript-why-its-critical-to-writing-good-code-7dca7eb489e7
作者:Austin Tackaberry,Human API 的软件工程师
译者:弯月,责编:屠敏
习目标:了解JavaScript是如何与HTML结合来创建动态网页,网页中嵌入JavaScript的不同方式,JavaScript的内容类型及其与<script>的关系
<script>是由Netscape创造出来,后来加到HTML规范中的。
<script>有8个属性:
1、async:表示立即开始下载脚本,但不能阻止其他页面动作,比如下载资源或者等待其他脚本加载。只对外部脚本文件有效。
2、charset:使用src属性指定代码字符集。这个属性很少用,因为大多数浏览器不在乎它的值。
3、crossorigin;配置资源请求的CORS(跨源资源共享)设置。默认情况下不使用CORS。crossorigin = “anonymous”配置文件请求不用设置凭据标志。crossorigin = ”use-credentials“设置凭据标志,意味着出站请求会包含凭据。
4、defer:表示脚本可以延迟到文档全部解析和显示后再执行。新版本中只能用于外部脚本。
5、integrity:允许比对接收到的资源和指定的加密签名以验证子资源完整性(SRI,Subresource integrity),如果验证签名不匹配则脚本不会执行。这个属性可以用于确保内容分发网络(CDN,Content Delivery Network)不会提供恶意内容。
6、language:此属性已被废止。
7、src:表示包含外部要执行的代码的外部文件。
8、type:代替language,表示代码块中脚本语言的内容类型(也称为MIME类型),按照惯例这个值始终都是”text/JavaScript“,尽管”text/JavaScript“和”text/ecmascript“都已经废弃。JavaScript文件的MIME类型通常是”application/x-javascript“,不过给type属性这个值的话可能会导致脚本被忽略。在非IE的浏览器中有效的值还有”application/JavaScript“和”application/ecmascript"。如果这个值是module,则代码会被当成是ES6模块,而且只有这时候代码中才能出现import和export关键字。
使用<script>的方式有内联和外嵌两种,只要把code写入<script>code</script>中就好,code中要是包含字符串“<script>”,只要加上转义字符“\”即可。
如果要外嵌JavaScript代码只要使用src属性来链接外部文件即可如:
<script src=“example.js”></script>
XHTML 文档中,可以忽略结束标签写成<script src=“example.js”/>即可,但是这在HTML中不能使用。
过去把JavaScript和CSS一起写在head中,但是这意味着必须下载所有code并解析和解释完成后才开始渲染页面,对于JavaScript很多的页面会导致页面渲染速度过慢,为解决这个问题,JavaScript一般写在body元素的页面内容的最后边,如下
<html>
<head></head>
<body>
message
<script>code<\script>
<\body>
</html>
在外联JavaScript时可以使用defer属性来推迟脚本的运行。可以写成:
<html>
<head>
<script defer src = "example.js">code<\script>
</head>
<body>
message
<\body>
</html>
async属性从脚本处理方式上与defer类似,但是不同的是标记async的脚本并不能保证脚本按照他们的出现顺序执行,比如:
<html>
<head>
<script sync src = "example1.js">code<\script>
<script sync src = "example2.js">code<\script>
</head>
<body>
message
<\body>
</html>
不能保证example1比example2先执行。
除了<script>以外还可以用其他方式加载脚本。因为JavaScript可以使用DOM API,所以通过向DOM中动态地加入script元素同样可以加载指定脚本。只要创建一个script元素并将其添加到DOM即可。
let script = document.createElement('script');
script.src = 'gibberish.js';
document.head.appendChild(script);
当然,在把 HTMLElement 元素添加到 DOM 且执行到这段代码之前不会发送请求。默认情况下,以这种方式创建的<script>元素是以异步方式加载的,相当于添加了 async 属性。不过这样做可能会有问题,因为所有浏览器都支持 createElement()方法,但不是所有浏览器都支持 async 属性。因此,如果要统一动态脚本的加载行为,可以明确将其设置为同步加载:
let script = document.createElement('script');
script.src = 'gibberish.js';
script.async = false;
document.head.appendChild(script);
以这种方式获取的资源对浏览器预加载器是不可见的。这会严重影响它们在资源获取队列中的优先级。根据应用程序的工作方式以及怎么使用,这种方式可能会严重影响性能。要想让预加载器知道这些动态请求文件的存在,可以在文档头部显式声明它们:
<link rel="preload" href="gibberish.js">
可扩展超文本标记语言(XHTML,Extensible HyperText Markup Language)是将 HTML 作为 XML的应用重新包装的结果。与 HTML 不同,在 XHTML 中使用 JavaScript 必须指定 type 属性且值为text/javascript,HTML 中则可以没有这个属性。XHTML 虽然已经退出历史舞台,但实践中偶尔可能也会遇到遗留代码,为此本节稍作介绍。在 XHTML 中编写代码的规则比 HTML 中严格,这会影响使用<script>元素嵌入 JavaScript 代码。下面的代码块虽然在 HTML 中有效,但在 XHML 中是无效的。
<script type="text/javascript">
function compare(a, b) {
if (a < b) {
console.log("A is less than B");
} else if (a > b) {
console.log("A is greater than B");
} else {
console.log("A is equal to B");
}
}
</script>
在 HTML 中,解析<script>元素会应用特殊规则。XHTML 中则没有这些规则。这意味着 a < b语句中的小于号(<)会被解释成一个标签的开始,并且由于作为标签开始的小于号后面不能有空格,这会导致语法错误。避免 XHTML 中这种语法错误的方法有两种。第一种是把所有小于号(<)都替换成对应的 HTML实体形式(<)。结果代码就是这样的:
<script type="text/javascript">
function compare(a, b) {
if (a < b) {
console.log("A is less than B");
} else if (a > b) {
console.log("A is greater than B");
} else {
console.log("A is equal to B");
}
}
</script>
这样代码就可以在 XHTML 页面中运行了。不过,缺点是会影响阅读。好在还有另一种方法。第二种方法是把所有代码都包含到一个 CDATA 块中。在 XHTML(及 XML)中,CDATA 块表示文档中可以包含任意文本的区块,其内容不作为标签来解析,因此可以在其中包含任意字符,包括小于号,并且不会引发语法错误。使用 CDATA 的格式如下:
<script type="text/javascript"><![CDATA[
function compare(a, b) {
if (a < b) {
console.log("A is less than B");
} else if (a > b) {
console.log("A is greater than B");
} else {
console.log("A is equal to B");
}
}
]]></script>
在兼容 XHTML 的浏览器中,这样能解决问题。但在不支持 CDATA 块的非 XHTML 兼容浏览器中则不行。为此,CDATA 标记必须使用 JavaScript 注释来抵消:
<script type="text/javascript">
//<![CDATA[
function compare(a, b) {
if (a < b) {
console.log("A is less than B");
} else if (a > b) {
console.log("A is greater than B");
} else {
console.log("A is equal to B");
}
}
//]]>
</script>
这种格式适用于所有现代浏览器。虽然有点黑科技的味道,但它可以通过 XHTML 验证,而且对XHTML 之前的浏览器也能优雅地降级。
自 1995 年 Netscape 2 发布以来,所有浏览器都将 JavaScript 作为默认的编程语言。type 属性使用一个 MIME 类型字符串来标识<script>的内容,但 MIME 类型并没有跨浏览器标准化。即使浏览器默认使用 JavaScript,在某些情况下某个无效或无法识别的 MIME 类型也可能导致浏览器跳过(不执行)相关代码。因此,除非你使用 XHTML 或<script>标签要求或包含非 JavaScript 代码,最佳做法是不指定 type 属性。在最初采用 script 元素时,它标志着开始走向与传统 HTML 解析不同的流程。对这个元素需要应用特殊的解析规则,而这在不支持 JavaScript 的浏览器(特别是 Mosaic)中会导致问题。不支持的浏览器会把<script>元素的内容输出到页面上,从而破坏页面的外观。Netscape 联合 Mosaic 拿出了一个解决方案,对不支持 JavaScript 的浏览器隐藏嵌入的 JavaScript 代码。最终方案是把脚本代码包含在一个 HTML 注释中,像这样:
<script><!--
function sayHi(){
console.log("Hi!");
}
//--></script>
使用这种格式,Mosaic 等浏览器就可以忽略<script>标签中的内容,而支持 JavaScript 的浏览器则必须识别这种模式,将其中的内容作为 JavaScript 来解析。虽然这种格式仍然可以被所有浏览器识别和解析,但已经不再必要,而且不应该再使用了。在XHTML 模式下,这种格式也会导致脚本被忽略,因为代码处于有效的 XML 注释当中。
虽然可以直接在 HTML 文件中嵌入 JavaScript 代码,但通常认为最佳实践是尽可能将 JavaScript 代码放在外部文件中。不过这个最佳实践并不是明确的强制性规则。推荐使用外部文件的理由如下。
可维护性。JavaScript 代码如果分散到很多 HTML 页面,会导致维护困难。而用一个目录保存所有 JavaScript 文件,则更容易维护,这样开发者就可以独立于使用它们的 HTML 页面来编辑代码。
缓存。浏览器会根据特定的设置缓存所有外部链接的 JavaScript 文件,这意味着如果两个页面都用到同一个文件,则该文件只需下载一次。这最终意味着页面加载更快。
适应未来。通过把 JavaScript 放到外部文件中,就不必考虑用 XHTML 或前面提到的注释黑科技。包含外部 JavaScript 文件的语法在 HTML 和 XHTML 中是一样的。在配置浏览器请求外部文件时,要重点考虑的一点是它们会占用多少带宽。在 SPDY/HTTP2 中,预请求的消耗已显著降低,以轻量、独立 JavaScript 组件形式向客户端送达脚本更具优势。比如,第一个页面包含如下脚本:
<script src="mainA.js"></script>
<script src="component1.js"></script>
<script src="component2.js"></script>
<script src="component3.js"></script>
...
后续页面可能包含如下脚本:
<script src="mainB.js"></script>
<script src="component3.js"></script>
<script src="component4.js"></script>
<script src="component5.js"></script>
...
在初次请求时,如果浏览器支持 SPDY/HTTP2,就可以从同一个地方取得一批文件,并将它们逐个放到浏览器缓存中。从浏览器角度看,通过 SPDY/HTTP2 获取所有这些独立的资源与获取一个大JavaScript 文件的延迟差不多。在第二个页面请求时,由于你已经把应用程序切割成了轻量可缓存的文件,第二个页面也依赖的某些组件此时已经存在于浏览器缓存中了。当然,这里假设浏览器支持 SPDY/HTTP2,只有比较新的浏览器才满足。如果你还想支持那些比较老的浏览器,可能还是用一个大文件更合适。
IE5.5 发明了文档模式的概念,即可以使用 doctype 切换文档模式。最初的文档模式有两种:混杂模式(quirks mode)和标准模式(standards mode)。前者让 IE 像 IE5 一样(支持一些非标准的特性),后者让 IE 具有兼容标准的行为。虽然这两种模式的主要区别只体现在通过 CSS 渲染的内容方面,但对JavaScript 也有一些关联影响,或称为副作用。本书会经常提到这些副作用。
IE 初次支持文档模式切换以后,其他浏览器也跟着实现了。随着浏览器的普遍实现,又出现了第三种文档模式:准标准模式(almost standards mode)。这种模式下的浏览器支持很多标准的特性,但是没有标准规定得那么严格。主要区别在于如何对待图片元素周围的空白(在表格中使用图片时最明显)。
混杂模式在所有浏览器中都以省略文档开头的 doctype 声明作为开关。这种约定并不合理,因为混杂模式在不同浏览器中的差异非常大,不使用黑科技基本上就没有浏览器一致性可言。标准模式通过下列几种文档类型声明开启:
<!-- HTML 4.01 Strict -->
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<!-- XHTML 1.0 Strict -->
<!DOCTYPE html PUBLIC
"-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<!-- HTML5 -->
<!DOCTYPE html>
准标准模式通过过渡性文档类型(Transitional)和框架集文档类型(Frameset)来触发:
<!-- HTML 4.01 Transitional -->
<!DOCTYPE HTML PUBLIC
"-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<!-- HTML 4.01 Frameset -->
<!DOCTYPE HTML PUBLIC
"-//W3C//DTD HTML 4.01 Frameset//EN"
"http://www.w3.org/TR/html4/frameset.dtd">
<!-- XHTML 1.0 Transitional -->
<!DOCTYPE html PUBLIC
"-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<!-- XHTML 1.0 Frameset -->
<!DOCTYPE html PUBLIC
"-//W3C//DTD XHTML 1.0 Frameset//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">
准标准模式与标准模式非常接近,很少需要区分。人们在说到“标准模式”时,可能指其中任何一个。而对文档模式的检测(本书后面会讨论)也不会区分它们。本书后面所说的标准模式,指的就是除混杂模式以外的模式。
针对早期浏览器不支持 JavaScript 的问题,需要一个页面优雅降级的处理方案。最终,<noscript>元素出现,被用于给不支持 JavaScript 的浏览器提供替代内容。虽然如今的浏览器已经 100%支持JavaScript,但对于禁用 JavaScript 的浏览器来说,这个元素仍然有它的用处。<noscript>元素可以包含任何可以出现在<body>中的 HTML 元素,<script>除外。在下列两种情况下,浏览器将显示包含在<noscript>中的内容:
浏览器不支持脚本;
浏览器对脚本的支持被关闭。任何一个条件被满足,包含在<noscript>中的内容就会被渲染。否则,浏览器不会渲染<noscript>中的内容。
下面是一个例子:
<!DOCTYPE html>
<html>
<head>
<title>Example HTML Page</title>
<script defer="defer" src="example1.js"></script>
<script defer="defer" src="example2.js"></script>
</head>
<body>
<noscript>
<p>This page requires a JavaScript-enabled browser.</p>
</noscript>
</body>
</html>
这个例子是在脚本不可用时让浏览器显示一段话。如果浏览器支持脚本,则用户永远不会看到它。
JavaScript 是通过<script>元素插入到 HTML 页面中的。这个元素可用于把 JavaScript 代码嵌入到HTML 页面中,跟其他标记混合在一起,也可用于引入保存在外部文件中的 JavaScript。本章的重点可以总结如下。
要包含外部 JavaScript 文件,必须将 src 属性设置为要包含文件的 URL。文件可以跟网页在同一台服务器上,也可以位于完全不同的域。
所有<script>元素会依照它们在网页中出现的次序被解释。在不使用 defer 和 async 属性的情况下,包含在<script>元素中的代码必须严格按次序解释。
对不推迟执行的脚本,浏览器必须解释完位于<script>元素中的代码,然后才能继续渲染页面的剩余部分。为此,通常应该把<script>元素放到页面末尾,介于主内容之后及</body>标签之前。
可以使用 defer 属性把脚本推迟到文档渲染完毕后再执行。推迟的脚本原则上按照它们被列出的次序执行。
可以使用 async 属性表示脚本不需要等待其他脚本,同时也不阻塞文档渲染,即异步加载。异步脚本不能保证按照它们在页面中出现的次序执行。
通过使用<noscript>元素,可以指定在浏览器不支持脚本时显示的内容。如果浏览器支持并启用脚本,则<noscript>元素中的任何内容都不会被渲染。
.JavaScript概念
JavaScript是一种基于原型的面向对象的语言。
同时,因为JavaScript是脚本语言,而脚本语言又是一种解释性的语言,
所以JavaScript是逐行执行的同时也具有解释型语言的特点:不需要编译可以直接使用,由解释器来负责解释。
2.JavaScript特点
(1)面向对象
JavaScript 语言标准已经明确说明,JavaScript 是一门面向对象的语言;
JavaScript是否属于“面向对象的语言”一直饱受争议,一些争论中,有人强调,JavaScript 并非“面向对象的语言”,而是“基于对象的语言”,这个说法也一度流传甚广。JavaScript 的对象设计跟目前主流基于类的面向对象差异非常大。而事实上,这样的对象系统设计虽然特别,但是 JavaScript 提供了完全运行时的对象系统,这使得它可以模仿多数面向对象编程范式,所以它也是正统的面向对象语言。而因为与基于类的面向对象的差异,我们也称JavaScript 为基于原型的面向对象。
(2)脚本语言
又被称为动态语言,只在被调用时进行解释或编译。在程序的运行过程中逐行解释执行。
(3)解释性语言
不需要编译可以直接使用,由宿主环境(浏览器)解释执行。
我自己是一名从事了多年开发的web前端老程序员,目前辞职在做自己的web前端私人定制课程,今年年初我花了一个月整理了一份最适合2019年学习的web前端学习干货,各种框架都有整理,送给每一位前端小伙伴,想要获取的可以关注我的头条号并在后台私信我:前端,即可免费获取。
(4)事件驱动
JavaScript对用户的响应,是以事件驱动的方式进行的。在网页(Web Page)中执行了某种操作所产生的动作,被称为“事件”(Event)。例如按下鼠标、移动窗口、选择菜单等都可以被视为事件。当事件发生后,可能会引起相应的事件响应,执行某些对应的脚本,这种机制被称为“事件驱动”。
(5)动态性
语言的动态性,是指程序在运行时可以改变其结构。在一个 JavaScript 对象中,要为一个属性赋值时,我们不必事先创建一个变量,只需要在使用的时候做赋值操作即可。
(6)弱类型&松散类型
弱类型语言声明数据时不需要指定数据类型,一个变量可以赋不同数据类型的值,不同类型数据在计算过程中会自动进行转换,在参与运算的过程中,JavaScript会将其自动转换为数据类型,比如JavaScript中布尔类型的数据可以直接参与运算。
(7)单线程与异步处理共存
单线程程序的执行顺序是从上到下依次执行,一个程序中只可以执行一个程序。而异步处理不用阻塞当前线程来等待处理完成,而是允许后续操作,直至其它线程将处理完成。二者看起来相互矛盾,不能够同时存在,可是JavaScript以一种巧妙地方式用单线程实现了异步处理的效果。
在JavaScript的主线程和任务队列中间还存在一个WebAPIs,这个WebAPIs是浏览器单独为处理JavaScript的异步任务开辟的线程。当主线程栈中的函数需要异步处理的时候,主线程会把需要异步的部分推给WebAPIs,这部分异步由浏览器去执行,执行完异步之后将处理后的结果以事件的形式丢到任务队列中,这个事件就是我们写代码时的“回调函数”。
(8)跨平台
JavaScript依赖于浏览器本身,与操作环境无关。
(9)安全性语言
JavaScript是一种安全性语言,它不允许访问本地的硬盘,并不能将数据存入到服务器上,不允许对网络文档进行修改和删除,只能通过浏览器实现信息浏览或动态交互。从而有效地防止数据的丢失。
3.javaScript的核心
JavaScript 的三大核心为ECMAScript、DOM 和 BOM。
ECMAScript(核心):
描述了该语言的语法和基本对象,ECMAScript是一种由Ecma国际(前身为欧洲计算机制造商协会,European Computer Manufacturers Association)通过ECMA-262标准化的脚本程序设计语言。这种语言在万维网上应用广泛,它往往被称为JavaScript或JScript,所以它可以理解为是JavaScript的一个标准,但实际上后两者是ECMA-262标准的实现和扩展。
DOM(文档对象模型):
描述了处理网页内容的方法和接口;是W3C组织推荐的处理可扩展置标语言的标准编程接口。是一种与平台和语言无关的应用程序接口(API),它可以动态地访问程序和脚本,更新其内容、结构和www文档的风格(目前,HTML和XML文档是通过说明部分定义的)。
BOM(浏览器对象模型):
描述了与浏览器进行交互的方法和接口;用于描述这种对象与对象之间层次关系的模型,浏览器对象模型提供了独立于内容的、可以与浏览器窗口进行互动的对象结构。BOM由多个对象组成,其中代表浏览器窗口的Window对象是BOM的顶层对象,其他对象都是该对象的子对象。
4.javaScript的使用
html页面中写在<script></script>标签内
引入外部.js文件
引入的多个js文件,会按顺序分开执行。同样的,对于不同<script>标签嵌入的JavaScript代码,也会根据脚本标签<script>的出现顺序来分开执行。由于JavaScript通常需要操作DOM,所以,一般把JavaScript放在前或者文档结尾处引入。若需要在中引入,可以通过修改window.onload或者document.ready事件,强制等到DOM加载完成后再执行相关函数。
5.javaScript的执行顺序
JavaScript代码执行分为两个部分:
预编译阶段,进行代码的检查装载,同样也是按从上到下按顺序进行的。此阶段只进行变量和函数的声明,会跳过执行语句,不对变量进行赋值,变量的默认值为undefined。即使声明是在调用的下方进行的,但浏览器仍然先声明再调用(执行),这个现象叫做“声明提升”。所以,即便一个函数的声明在函数调用的下方,前面仍然可以正常执行这个函数,需要注意的是函数声明的提升优先于变量声明的提升。(js代码的执行是读一行代码执行一行,但在执行之前系统会先对js进行全面扫描检查是否存在低级的语法错误,并不会立即执行语句。)
原文链接:https://blog.csdn.net/qq_28453669/article/details/103211948
作者:Simeow
*请认真填写需求信息,我们会在24小时内与您取得联系。