如何用 JavaScript 写一个 URL 拼接方法?

2021/06/09

「如何」系列第五篇,五个步骤完成了一道经典前端面试题。

前言

URL 拼接是一个经典的前端面试题,它除了考察面试者对各种边界情况考虑的细致程度,也涉及到【数组方法】、【原型链】等基本知识。

一、问题

现有一串url(string 类型),和一个查询对象newQuery(JSON ),请编写一个函数,输入urlnewQuery,输出新的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
}

二、分析

流程图

未命名文件 (2)

坑点

  • 输入是否为空?==> 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)// => '√'

所以,

  1. 检测url是否为空字符串 ,可以直接使用原值判断
  2. 检测newQuery是否为空对象 ,可以使用
    1. Object.prototype.tostring.call(newQuery)newQuery.toString()
    2. JSON.stringfy(newQuery)

3.2 第二步:原 url 解析

const url = "http://example.com/pathname/query?d=1&e=2"

要解析如上的url输入,我们需要对其进行预处理,拆分为两个部分

url (1)

在浏览器实现中,有一种巧妙的方式来拆分原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)

四、总结

代码不长,但花了我很长时间来查证细节,发现自己对一些数组方法熟悉程度还是不够,比如数组和字符串有一些方法同名且用法类似(比如indexofslice),有一些则不通用(比如属于数组的splice),具体可以参考另一篇文章。另外,对于原url不含query的情况没有在文章中展现,只需要在3.2 第二步中加一层判断即可,此处不再赘述。

参考

JavaScript encodeURIComponent() 函数

js数组与字符串类型相同方法的比较

JS中解析URL的简单方法