JS-Web-API 知识点

140 阅读10分钟

除ES基础之外,Web前端经常会⽤到⼀些跟浏览器相关的API

知识点梳理

  • BOM操作
  • DOM操作
  • 事件绑定
  • Ajax
  • 存储

BOM

BOM(浏览器对象模型)是浏览器本身的⼀些信息的设置和获取,例如获取浏览器的宽度、高度,设置让浏览器跳转到哪个地址。

  • navigator
  • screen
  • location
  • history

这些对象就是⼀堆⾮常简单粗暴的API,去MDN或者w3school这种⽹站⼀查就都明⽩了。下⾯列举⼀下常⽤功能的代码示例获取浏览器特性(即俗称的UA)然后识别客户端,例如判断是不是Chrome浏览器。

获取浏览器特性(即俗称的UA)然后识别客户端,例如判断是不是Chrome浏览器。

var ua = navigator.userAgent
var isChrome = ua.indexOf('Chrome')
console.log(isChrome)

获取屏幕的宽度和高度:

console.log(screen.width)
console.log(screen.height)

获取⽹址、协议、path、参数、hash等:

//例如当前⽹址是 https://juejin.im/timeline/frontend?a=10&b=10#some
console.log(location.href) // https://juejin.im/timeline/frontend?a=10&b=10#some
console.log(location.protocol) // https:
console.log(location.search) // ?a=10&b=10
console.log(location.hash) // #some

另外,还有浏览器的前进、后退功能等:

history.back()
history.forward()

DOM

题⽬:DOM和HTML区别和联系

什么是DOM

讲DOM先从HTML讲起,讲HTML先从XML讲起。XML是⼀种可扩展的标记语⾔,所谓可扩展就是它可以描述任何结构化的数据,它是⼀棵树!

<?xml version="1.0" encoding="UTF-8" ?>
<note>
    <to>Tove</to>
    <from>Jani</from>
    <heading>Reminder</heading>
    <body>Don't forget me this weekend!</body>
    <other>
        <a></a>
        <b></b>
    </other>
</note>

HTML是⼀个有既定标签标准的XML格式,标签的名字、层级关系和属性,都被标准化(否则浏览器⽆法解析)。同样,它也是⼀棵树。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <div>
        <p>this is p</p>
    </div>
</body>
</html>

开发完的HTML代码会保存到⼀个⽂档中(⼀般以.html或者.htm结尾),⽂档放在服务器上,浏览器请求服务器,这个⽂档被返回。因此,最终浏览器拿到的是⼀个⽂档⽽已,⽂档的内容就是HTML格式的代码。

但是浏览器要把这个⽂档中的HTML按照标准渲染成⼀个⻚⾯,此时浏览器就需要将这堆代码处理成⾃⼰能理解的东⻄,也得处理成JS能理解的东⻄,因为还得允许JS修改⻚⾯内容呢。

基于以上需求,浏览器就需要把HTML转变成DOM,HTML是⼀棵树,DOM也是⼀棵树。对DOM的理解,可以暂时先抛开浏览器的内部因素,先从JS着⼿,即可以认为DOM就是JS能识别的HTML结构,⼀个普通的JS对象或者数组。

image.png

获取DMO节点

最常⽤的DOM API就是获取节点,其中常⽤的获取⽅法如下⾯代码示例:

// 通过 id 获取
var div1 = document.getElementsById('div1') // 元素

// 通过 tagname 获取
var divList = document.getElementsByClassName('div') // 集合
console.log(divList.length)
console.log(divList[0])

// 获取 class 获取
var containerList = document.getElementsByClassName('containerList') // 集合

// 通过 CSS 选择期获取
var pList = document.querySelectorAll('p') // 集合

题目:property和attribute的区别是什么?

property

DOM节点就是⼀个JS对象,它符合之前讲述的对象的特征——可扩展属性,因为DOM节点本质上也是⼀个JS对象。因此,如下代码所示,p可以有style属性,有classNamenodeNamenodeType属性。注意,这些都是JS范畴的属性,符合JS语法标准的

var pList = document.querySelectorAll('p')
var p = pList[0]
console.log(p.style.width) // 修改样式
console.log(p.className);  // 获取 class
p.className = 'p1'         // 修改 class

// 获取 nodeName 和 nodeType
console.log(p.nodeName)
console.log(p.nodeType)

attribute

property获取和修改,是直接改变JS对象,而attribute直接改变HTML的属性,两种有很大的区别。attribute就是对HTML属性的get和set,和DOM节点的JS范畴的property没有关系。

var pList = document.querySelectorAll('p')
var p = pList[0]
p.getAttribute('data-name')
p.setAttribute('data-name', 'juejin')
p.getAttribute('style')
p.setAttribute('style', 'font-size:30px')

而且,get和set attribute时,还会触发DOM的查询或者重绘、重排,频繁操作会影响⻚⾯性能。

题目:DOM操作的基本API有哪些?

DOM树操作

新增节点

var div1 = document.getElementById('div1')

// 新增新节点
var p1 = document.createElement('p')
p1.innerHTML = 'this is p1'
div1.appendChild(p1) //添加新创建的元素

// 移动已有的父节点。注意,这里的"移动",并不是拷贝
var p2 = document.getElementById('p2')
div1.appendChild(p2)

获取父节点

var div1 = document.getElementById('div1')
var parent = document.parentElement

获取子节点

var div1 = document.getElementById('div1')
var child = div1.childNodes

删除节点

var div1 = document.getElementById('div1')
var child = div1.childNodes
div1.removeChild(child[0])

还有其他操作的API,例如获取前⼀个节点、获取后一个节点等.

事件

事件绑定

普通的事件绑定写法如下:

var btn = document.getElementById('btn')
btn.addEventListener('click', function (event) {
    // event.preventDefault()   // 阻止默认行为
    // event.stopPropagation()  // 阻止冒泡
    console.log('clicked');
})

为了编写简单的事件绑定,可以编写通⽤的事件绑定函数。这里虽然比较简单,后面再完善。

// 通用的事件绑定函数
function bindEvent(el, type, fn) {
   el.addEventLister(type, fn)
}

var a = document.getElementById('link')
// 写起来更加简单了
bindEvent(a, 'click', function(e) {
   e.preventDefault();
   alter('clicked')
})

题目:什么是事件冒泡?

 <div id="div1">
   <p id="p1">激活</p>
   <p id="p2">取消</p>
   <p id="p3">取消</p>
   <p id="p4">取消</p>
</div>
<div id="div2">
   <p id="p5">取消</p>
   <p id="p6">取消</p>
</div>

对于以上HTML代码结构,要求点击p1时候进激活状态,点击其他任何<p>都取消激活状态,如何实现?代码如下,注意看注释:

var body = document.body
bindEvent(body, 'click', function (e) {
    // 所有p的点击都会冒泡到body上, 因为DOM结构中body是p的上级节点
    // 事件会沿着DOM树向上冒泡
    alert('取消')
  })

var p1 = document.getElementById('p1')
bindEvent(body, 'click', function (e) {
    e.stopPropagation(); // 阻止冒泡
    alert('激活')
})

如果我们在p1 div1 body中都绑定了事件,它是会根据DOM的结构来冒泡,从下到上挨个执行e.stopPropagation()就可以阻止冒泡。

题目:如何使⽤事件代理?有何好处?

事件代理

设定一种场景,如下代码,一个<div>中包含了若干个<a>,⽽且还能继续增加。那如何快捷⽅便地为所有<a>绑定事件呢?

<div id="div1">
  <a href="#">a1</a>
  <a href="#">a2</a>
  <a href="#">a3</a>
  <a href="#">a4</a>
</div>
<button>点击添加一个 a 标签</button>

这里就会用到事件代理。我们要监听<a>的事件,但要把具体的事件绑定到<div>上,然后看事件的触发点是不是<a>

var div1 = document.getElementById('div1')
div1.addEventListener('click', function (e) {
    // e.target 可以监听到出发点击事件的元素是哪一个
    var target = e.target
    if (e.nodeName === 'A') {
        // 点击的是 <a> 元素
        alert(target.innerHTML)
    }
})

现在完善一下之前写的通⽤事件绑定函数,加上事件代理。

function bindEvent(elem, type, selector, fn) {
    if (fn == null) {
        fn = selector
        selector = null
    }

    // 绑定事件
    elem.addEventListener(type, function(e) {
        var target
        if (selector) {
            // 有 slector 说明需要做的事件代理
            // 获取触发时间的元素,即 e.target
            target = e.target
                // 看是否符合 selector 这个条件
            if (target.matches(selector)) {
                fn.call(target, e)
            }
        } else {
            // 无 selector,说明不需要事件代理
            fn(e)
        }
    })
}

然后这样使用,简单很多。

// 使用代理,bindEvent 多一个 'a'参数
var div1 = document.getElementById('div1')
bindEvent(div1, 'click', 'a', function(e) {
    console.log(this.innerHTML);
})


//不使用代理
var a = document.getElementById('a')
bindEvent(div1, 'click', function(e) {
    console.log(this.innerHTML);
})

最后,使用代理的优点如下:

  • 使代码简洁
  • 减少浏览器的内存占用

Ajax

题目:手写XMLHttpRequest不借助任何库

var xhr = new XMLHttpRequest()
xhr.onreadystatechange = function() {
    // 这里的函数异步执行
    if (xhr.readyState == 4) {
        if (xhr.status == 200) {
            alert(xhr.responseText)
        }
    }
}

xhr.open('GET', '/api', false)
xhr.send(null)

当然,使用jQueryZeptoFetch等库来写就更加简单了。

状态码说明

上述代码中,有两处状态码需要说明。xhr.readyState是浏览器判断请求过程中各个阶段的,xhr.status是HTTP协议中规定的不同结果的返回状态说明。

xhr.readyState的状态码说明:

  • 0-代理被创建,但尚未调用open()方法。
  • 1 - open()方法已经被调用。
  • 2 - send()方法已经被调用,并且头部和状态已经可获得。
  • 3 - 下载中,responseText属性已经包含部分数据。
  • 4 - 下载操作已完成

题目:HTTP协议中,response的状态码,常见的有哪些?

xhr.status即HTTP状态码,有2xx 3xx 4xx 5xx 这几种,比较常见的有以下几种:

  • 200 正常
  • 3xx
    • 301 永久重定向。如http://xxx.com这个GET请求(最后没有/),就会被301到http://xxx.com/(最后是/
    • 302 临时重定向。临时的,不是永久的
    • 304 资源找到但是不符合请求条件,不会返回任何主体。如发送GET请求时,head中有If-Modified-Since: xxx(要求返回更新时间是xxx时间之后的资源),如果此时服务器端资源未更新,则会返回304,即不符合要求
  • 404 找不到资源
  • 5xx 服务器端出错了

理解为何上述代码中要同时满足xhr.readyState == 4xhr.status == 200

Fetch API

⽬前已经有个获取HTTP请求更加⽅便的API:Fetch,通过Fetch提供的fetch()这个全局函数⽅法可以很简单地发起异步请求,并且支持Promise的回调。但是Fetch API是比较新的API,具体使⽤的时候还需要查查caniuse,看下其浏览器兼容情况。

fetch('some/api/data.json', {
    method: 'POST',
    headers: {},
    body: {},
    mode: '',
    credentials:'',
    cache: '',
}).then(res => {})

Fetch支持headers定义,通过headers⾃定义可以⽅便地实现多种请求⽅法(PUT、GET、POST等)、请求头(包括跨域)和cache策略等;除此之外还⽀持response(返回数据)多种类型,比如支持二进制文件、字符串和formData等。

跨域

题目:如何实现跨域?

浏览器中有同源策略,即一个域下的页面中,无法通过Ajax获取到其他域的接口。例如有一个接口http://m.juejin.com/course/ajaxcourserecom?cid=459,你自己的个页面http://www.yourname.com/page1.html中的Ajax无法获取这个接口。这正是命中了“同源策略”。如果浏览器哪些地方忽略了同源策略,那就是浏览器的安全漏洞,需要紧急修复。

url哪些地⽅不同算作跨域?

  • 协议
  • 域名
  • 端口

但是HTML中几个标签能逃避过同源策略——<script src="xxx"><imgsrc="xxxx"/><link href="xxxx">,这三个标签的src/href可以加载其他域的资源,不受同源策略限制。因此,这使得这三个标签可以做⼀些特殊的事情。<img>可以做打点统计,

除了能跨域之外,几乎没有浏览器兼容问题,它是一个非常古老的标签。<script><link>可以使用CDN,CDN基本都是其他域的链接。另外<script>还可以实现JSONP,能获取其他域接口的信息。

但是请注意,所有的跨域请求方式,最终都需要信息提供⽅来做出相应的⽀持和改动,也就是要经过信息提供方的同意才行,否则接收方是无法得到它们的信息的,浏览器是不允许的。

解决跨域- JSONP

首先,有一个概念要明白,例如访问http://coding.m.juejin.com/classindex.html的时候,服务器端就一定有一个classindex.html文件吗?——不一定,服务器可以拿到这个请求,动态生成一个文件,然后返回。同理,<script src="http://coding.m.juejin.com/api.js"></script> 也不一定加载一个服务器端的静态文件,服务器也可以动态⽣成⽂件并返回。OK,接下来正式开始。例如我们的网站和掘金网,肯定不是这个域。我们需要掘金网提供一个接口,供我们来获取。首先,我们在自己的页面这样定义。

<script>
window.callback = function(data) {
    //这是跨域得到的数据
    console.log(data);
}
</script>

我们在⾃⼰的⻚⾯这样定义 然后掘⾦⽹给我提供了⼀个 http://coding.m.juejin.com/api.js , 内容如下 (之前说过, 服务 器可动态⽣成内容)

callback({x: 100, y:200})

最后我们在⻚⾯中加⼊ <script src="http://coding.m.juejin.com/api.js"></script> , 那 么这个 js 加载之后, 就会执⾏内容, 我们就得到内容了。

解决跨域 - 服务器端设置 http header

这是需要在服务器端设置的,⽽且,现在推崇的跨域解决⽅案是这⼀种, ⽐JSONP简单许多。

response.setHeader("Access-Control-Allow-Origin", "http://m.juejin.com/"); // 第⼆个参数填写允许跨域的域名称, 不建议直接写 "*" 
response.setHeader("Access-Control-Allow-Headers", "X-Requested-With"); 
response.setHeader("Access-Control-Allow-Methods", " PUT,POST,GET,DELETE,OPTIONS"); // 接收跨域的 cookie 
response.setHeader("Access-Control-Allow-Credentials", "true");

存储

题⽬: cookie 和 localStorage 有何区别?

cookie 本身不是⽤来做服务器端存储的(计算机领域有很多这种 “ 狗拿耗⼦ ” 的例⼦, 例如 CSS 中的 float ), 它是设计⽤来在服务器和客户端进⾏信息传递的, 因此我们的每个 HTTP 请求都带着 cookie 。 但是 cookie 也具备浏览器端存储的能⼒ (例如记住⽤户名和密码),因此就被开发者⽤上了。

使⽤起来也⾮常简单,document.cookie = . . . .即可。

但是 cookie 有它致命的缺点:

  • 存储量太⼩,只有 4KB
  • 所有HTTP请求都带着,会影响获取资源的效率
  • API简单,需要封装才能⽤

localStroage 和 sessionStorage

后来,HTML5 标准就带来了 sessionStoragelocalStorage , 先拿 localStorage 来说, 它 是专⻔为了浏览器端缓存⽽设计的。其优点有:

  • 存储量增⼤到5MB
  • 不会带到 HTTP 请求中
  • API 适⽤于数据存储 localStorage.setItem(key, value) localStorage.getItem(key)

sessionStorage 的区别就在于它是根据 session 过去时间⽽实现, ⽽ localStorage 会永久有 效, 应⽤场景不同。例如, ⼀些需要及时失效的重要信息放在 sessionStorage 中, ⼀些不重要但 是不经常设置的信息, 放在 localStorage 中。

另外有个⼩技巧, 针对 localStorage.setItem , 使⽤时尽量加⼊到 try-catch 中,某些浏览器是禁⽤这个API的, 要注意。

小结

⽇常开发中最常⽤的 API 和知识。