如何用 JavaScript 写一个 URL 拼接方法?
「如何」系列第五篇,五个步骤完成了一道经典前端面试题。
前言
URL 拼接是一个经典的前端面试题,它除了考察面试者对各种边界情况考虑的细致程度,也涉及到【数组方法】、【原型链】等基本知识。
一、问题
现有一串url(string 类型),和一个查询对象newQuery(JSON ),请编写一个函数,输入url和newQuery,输出新的url:
const newQuery = {
a:1,
b:"NewString",
d:"?&*:;",
e:[11,22,33],
f:false,
g:null,
h:undefined
}
const url = "http://example.com/pathname/query?d=1&e=2"
function concatQuery = (url,newQuery)=>{
//...
return newUrl
}
二、分析
流程图
.png)
坑点
- 输入是否为空?==> JavaScript 空值检测/空对象检测
- 新旧 query 部分是否有重名?==> 利用对象键值的唯一性去重
- 特殊字符处理 ==> 正则过滤特殊字符/url 内容转义
三、步骤详解
3.1 第一步:空值检测
function concatQuery(){
if(!url){
return 'The origin url is empty'
}
if(newQuery.toString()=='{}'){
return url
}else{
//...
}
}
注意对newQuery判断的方法,因为涉及到隐式转换,if(value)和if(value==true)不适宜于判断空对象,会出现像下面这个例子展示的问题:
let empty = {}
let result = ''
if(empty==true){
result = 'It is true'
}else if (empty==false){
result = 'It is false'
}else if (!empty){
result = '×'
}else if (empty){
result = '√'
}
console.log(result)// => '√'
所以,
- 检测
url是否为空字符串 ,可以直接使用原值判断 - 检测
newQuery是否为空对象 ,可以使用Object.prototype.tostring.call(newQuery)或newQuery.toString()JSON.stringfy(newQuery)
3.2 第二步:原 url 解析
const url = "http://example.com/pathname/query?d=1&e=2"
要解析如上的url输入,我们需要对其进行预处理,拆分为两个部分
.png)
在浏览器实现中,有一种巧妙的方式来拆分原url
var parser = document.createElement('a');
parser.href = "http://example.com/pathname/query?d=1&e=2"
parser.origin; // => "http://example.com"
parser.pathname; // => "/pathname/query"
parser.search; // => "?d=1&e=2"
let urlLocation = parser.origin + parser.pathname
let queryString = parser.search.slice(1)//截取问号后的部分
当然,也可以直接使用?作为分隔符来拆分
const url = "http://example.com/pathname/query?d=1&e=2"
let splitUrl = url.split('?')
// => ["http://example.com/pathname/query", "d=1&e=2"]
let urlLocation = splitUrl[0]
let queryString = splitUrl[1]
对于得到的query部分的字符串,再对其进行对象化
let queryObj = {}
let queryArray = queryString.split('&')
// => ["d=1", "e=2"]
for (let value of queryArray){
valueArray = value.split('=')
queryObj[valueArray[0]] = valueArray[1]
}
console.log(queryObj)
// => {d:"1", e="2"}
到这一步我们就完成了对原url的所有解析过程,网上找了一下,有一些url解析的轮子,但普遍不包含query字符串部分的解析,因为这部分大概率是后端的业务场景。
3.3 第三步:对象合并
接下来需要去合并新旧两个query对象,使用析构操作符...,可以对两个可能存在同名属性的对象进行去重+合并 。
let queryObj = {
a:undefined,
b:"OldString",
c:5,
d:"DDDD"
}
const newQuery = {
a:1,
b:"NewString",
d:"?&*:;",
e:[11,22,33],
f:false,
g:null,
h:undefined
}
queryObj = {
...queryObj,
...newQuery
}
/*
==>
{
a: 1,
b: "2",
c: 5,
d: "?&*:;",
e: [11, 22, 33],
f: false,
g: null,
h: undefined
}
*/
3.4 第四步:字符串生成
const queryObj = { a: 1, b: "2", c: 5, d: "?&*:;", e: [11, 22, 33], f: false, g: null, h: undefined}
这一步是3.2 原 url 解析的逆过程,需要将新生成的query对象转成字符串,对于每个需要传递的字段,
使用encodeURIComponent()进行一次转义:
let newQueryString = ""for(let key in queryObj){ let valueStr = Object.prototype.toString.call(queryObj[key]) // 过滤掉值为 null 或者 undifined 的字段,将其余字段拼接 if(valueStr!="[object Null]"&& valueStr!="[object Undefined]"){ newQueryString = newQueryString + key + "=" + encodeURIComponent(queryObj[key]) + "&" }}console.log(newQueryString)// => a=1&b=2&c=5&d=%3F%26*%3A%3B&e=11%2C22%2C33&f=false&
3.5 第五步: url 生成
将3.2 第二步中拆分放在一边的urlLocation与新生成的queryString进行合并然后输出
//urlLocation = "http://example.com/pathname/query"//queryString = "a=1&b=2&c=5&d=%3F%26*%3A%3B&e=11%2C22%2C33&f=false&"let newUrl = urlLocation + "?" + newQueryStringreturn newUrl
3.6 完整代码
大功告成!完整代码如下:
const newQuery = { a:1, b:"NewString", d:"?&*:;", e:[11,22,33], f:false, g:null, h:undefined}const url = "http://example.com/pathname/query?d=1&e=2"function concatQuery(url, newQuery){ if(!url){ return 'The origin url is empty' } if(newQuery.toString()=='{}'){ return url }else{ let splitUrl = url.split('?') let urlLocation = splitUrl[0] let queryString = splitUrl[1] let queryObj = {} let queryArray = queryString.split('&') for (let value of queryArray){ valueArray = value.split('=') queryObj[valueArray[0]] = valueArray[1] } queryObj = { ...queryObj, ...newQuery } let newQueryString = "" for(let key in queryObj){ let valueStr = Object.prototype.toString.call(queryObj[key]) if(valueStr!="[object Null]"&& valueStr!="[object Undefined]"){ newQueryString = newQueryString + key + "=" + encodeURIComponent(queryObj[key]) + "&" } } let newUrl = urlLocation + "?" + newQueryString return newUrl }}let newUrl = concatQuery(url, newQuery)console.log(newUrl)
四、总结
代码不长,但花了我很长时间来查证细节,发现自己对一些数组方法熟悉程度还是不够,比如数组和字符串有一些方法同名且用法类似(比如indexof,slice),有一些则不通用(比如属于数组的splice),具体可以参考另一篇文章。另外,对于原url不含query的情况没有在文章中展现,只需要在3.2 第二步中加一层判断即可,此处不再赘述。
参考