JS高阶(八)文件上传

98 阅读5分钟

文件上传

axios二次封装

let instance = axios.create();
instance.defaults.baseURL = 'http://127.0.0.1:8888';
instance.defaults.headers['Content-Type'] = 'multipart/form-data';
instance.defaults.transformRequest = (data, headers) => {
    const contentType = headers['Content-Type'];
    if (contentType === "application/x-www-form-urlencoded") return Qs.stringify(data);
    return data
}
instance.interceptors.response.use(response => {
    return response.data;
})

前端页面

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>珠峰在线Web高级课</title>

    <!-- IMPORT CSS -->
    <link rel="stylesheet" href="css/reset.min.css">
    <link rel="stylesheet" href="css/upload.css">
    <style>
        html,
        body {
            overflow-x: hidden;
        }

        .container {
            padding: 20px 100px;
            display: flex;
            justify-content: space-between;
            align-items: flex-start;
        }

        .container .item h3 {
            line-height: 40px;
        }
    </style>
</head>

<body>
...

    <!-- IMPORT JS -->
    <script src="node_modules/axios/dist/axios.min.js"></script>
    <script src="node_modules/qs/dist/qs.js"></script>
    <script src="node_modules/spark-md5/spark-md5.min.js"></script>
    <script src="js/utils.min.js"></script>
    <script src="js/instance.js"></script>
    <script src="js/upload.js"></script>
</body>

</html>

基于FORM-DATA实现文件上传

      <div class="item">
            <h3>单一文件上传「FORM-DATA」</h3>
            <section class="upload_box" id="upload1">
                <!-- accept=".png" 限定上传文件的格式 -->
                <input type="file" class="upload_inp" accept=".png,.jpg,.jpeg">
                <div class="upload_button_box">
                    <button type="button"  class="upload_button select">选择文件</button>
                    <button type="button"  class="upload_button upload">上传到服务器</button>
                </div>
                <div class="upload_tip">只能上传 PNG/JPG/JPEG 格式图片,且大小不能超过2MB</div>
                <ul class="upload_list">
                    <!-- <li>
                        <span>文件:...</span>
                        <span><em>移除</em></span>
                    </li> -->
                </ul>
            </section>
        </div>
(function () {
  let upload = document.querySelector("#upload1"),
    upload_inp = upload.querySelector(".upload_inp"),
    upload_button_select = upload.querySelector(".upload_button.select"),
    upload_button_upload = upload.querySelector('.upload_button.upload'),
    upload_tip = upload.querySelector('.upload_tip'),
    upload_list = upload.querySelector('.upload_list');
  let _file = null;

  // 点击选择文件按钮,触发上传文件INPUT框选择文件的行为
  upload_button_select.addEventListener('click', () => {
    if (upload_button_select.classList.contains('disable') || upload_button_upload.classList.contains('loading')) return;
    upload_inp.click();
  })

  // 监听用户选择文件的操作
  upload_inp.addEventListener('change', () => {
    // 获取用户选中的文件对象
    //   + name:文件名
    //   + size:文件大小 B
    //   + type:文件的MIME类型
    let file = upload_inp.files[0];
    if (!file) return;

    // 限制文件上传的格式「方案一」
    /*     if (!/(PNG|JPG|JPEG)/i.test(typeof file.type)) {
          alert('上传的文件只能是 PNG/JPG/JPEG 格式的~~');
          return;
        } */

    // 限制文件上传的大小
    if (2 * 1024 * 1024 < file.size) {
      alert('上传的文件不能超过2MB~~');
      return;
    }

    _file = file;

    // 显示上传的文件
    upload_tip.style.display = 'none';
    upload_list.style.display = 'block';
    upload_list.innerHTML = `<li>
      <span>文件:${file.name}</span>
      <span><em>移除</em></span>
    </li>`;
  })

  // 移除按钮的点击处理
  const clearHandle = () => {
    _file = null;
    upload_tip.style.display = 'block';
    upload_list.style.display = 'none';
    upload_list.innerHTML = '';
    upload_inp.value = ''
  }
  upload_list.addEventListener('click', (ev) => {
    console.log(ev.target);
    let target = ev.target;
    if (target.tagName == "EM") clearHandle();
    return
  })

  // 上传文件到服务器
  const changeDisable = flag => {
    if (flag) {
      upload_button_select.classList.add('disable');
      upload_button_upload.classList.add('loading');
      return;
    }
    upload_button_select.classList.remove('disable');
    upload_button_upload.classList.remove('loading');
  }
  upload_button_upload.addEventListener('click', () => {
    if (upload_button_upload.classList.contains('disable') || upload_button_upload.classList.contains('loading')) return;
    if (!_file) {
      alert('请您先选择要上传的文件~~');
      return;
    }
    changeDisable(true);
    // 把文件传递给服务器:FormData / BASE64
    let formData = new FormData();
    formData.append('file', _file);
    formData.append('filename', _file.name);
    instance.post('/upload_single', formData).then(data => {
      if (+data.code == 0) {
        alert(`文件已经上传成功~~,您可以基于 ${data.servicePath} 访问这个资源~~`);
        return;
      }
      return Promise.reject(data.codeText)
    }).catch(reason => {
      alert('文件上传失败,请您稍后再试~~');
    }).finally(() => {
      clearHandle();
      changeDisable(false);
    })
  })
})();

基于BASE64实现文件上传

<div class="item">
            <h3>单一文件上传「BASE64」,只适合图片</h3>
            <section class="upload_box" id="upload2">
                <input type="file" class="upload_inp" accept=".jpg,.jpeg,.png">
                <div class="upload_button_box">
                    <button type="button"  class="upload_button select">上传图片</button>
                </div>
                <div class="upload_tip">只能上传jpg/png格式图片,且大小不能超过2mb</div>
            </section>
        </div>
(function () {
  let upload = document.querySelector("#upload2"),
    upload_inp = upload.querySelector(".upload_inp"),
    upload_button_select = upload.querySelector(".upload_button.select");

  // 验证是否处于可操作性状态
  const checkIsDisable = element => {
    let classList = element.classList;
    return classList.contains('disable') || classList.contains('loading');
  }

  // 点击选择文件按钮,触发上传文件INPUT框选择文件的行为
  upload_button_select.addEventListener('click', function () {
    if (checkIsDisable(this)) return;
    upload_inp.click();
  })

  // 把选择的文件读取成为BASE64
  const changeBASE64 = file => {
    return new Promise(resolve => {
      let fileReader = new FileReader();
      fileReader.readAsDataURL(file);
      fileReader.onload = ev => {
        resolve(ev.target.result);
      }
    })
  }

  // 上传文件
  upload_inp.addEventListener('change', async () => {
    let file = upload_inp.files[0],
      BASE64,
      data;
    if (!file) return;

    if (2 * 1024 * 1024 < file.size) {
      alert('上传的文件不能超过2MB~~');
      return;
    }

    upload_button_select.classList.add('loading');
    BASE64 = await changeBASE64(file);
    try {
      data = await instance.post('/upload_single_base64', {
        file: encodeURIComponent(BASE64),
        filename: file.name
      }, {
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded'
        }
      });
      if (+data.code == 0) {
        alert(`恭喜您,文件上传成功,您可以基于 ${data.servicePath} 地址去访问~~`);
        return;
      }
      throw data.codeText;
    } catch (err) {
      alert('很遗憾,文件上传失败,请您稍后再试~~');
    } finally {
      upload_button_select.classList.remove('loading');
      upload_inp.value = '';
    }
  })
})();

文件缩略图 & 自动生成名字

<div class="item">
            <h3>单一文件上传「缩略图处理」</h3>
            <section class="upload_box" id="upload3">
                <input type="file" class="upload_inp" accept=".jpg,.jpeg,.png">
                <div class="upload_button_box">
                    <button type="button"  class="upload_button select">选择文件</button>
                    <button type="button"  class="upload_button upload">上传到服务器</button>
                </div>
                <div class="upload_abbre">
                    <img src="" alt="">
                </div>
            </section>
        </div>
(function () {
  let upload = document.querySelector("#upload3"),
    upload_inp = upload.querySelector(".upload_inp"),
    upload_button_select = upload.querySelector(".upload_button.select"),
    upload_button_upload = upload.querySelector('.upload_button.upload'),
    upload_abbre = upload.querySelector('.upload_abbre'),
    upload_abbre_img = upload_abbre.querySelector('img');
  let _file = null;

  // 验证是否处于可操作性状态
  const checkIsDisable = element => {
    let classList = element.classList;
    return classList.contains('disable') || classList.contains('loading');
  }

  // 把选择的文件读取成为BASE64
  const changeBASE64 = file => {
    return new Promise(resolve => {
      let fileReader = new FileReader();
      fileReader.readAsDataURL(file);
      fileReader.onload = ev => {
        resolve(ev.target.result)
      }
    })
  }

  // 把选择的文件读取成为Buffer
  const changeBuffer = file => {
    return new Promise(resolve => {
      let fileReader = new FileReader();
      fileReader.readAsArrayBuffer(file);
      fileReader.onload = ev => {
        let buffer = ev.target.result,
          spark = new SparkMD5.ArrayBuffer(),
          HASH,
          suffix;
        spark.append(buffer);
        HASH = spark.end();
        suffix = /\.([a-zA-Z0-9]+)$/.exec(file.name)[1];
        resolve({
          buffer,
          HASH,
          suffix,
          filename: `${HASH}.${suffix}`
        });
      }
    })
  }

  // 点击选择文件按钮,触发上传文件INPUT框选择文件的行为
  upload_button_select.addEventListener('click', function () {
    if (checkIsDisable(this)) return;
    upload_inp.click();
  })

  // 文件预览,就是把文件对象转换为BASE64,赋值给图片的SRC属性即可
  upload_inp.addEventListener('change', async () => {
    let file = upload_inp.files[0],
      BASE64;
    if (!file) return;
    _file = file;
    upload_button_select.classList.add('disable');
    BASE64 = await changeBASE64(file);
    upload_abbre.style.display = 'block';
    upload_abbre_img.src = BASE64;
    upload_button_select.classList.remove('disable');
  })

  // 上传文件到服务器
  const changeDisable = flag => {
    if (flag) {
      upload_button_select.classList.add('disable');
      upload_button_upload.classList.add('loading');
      return;
    }
    upload_button_select.classList.remove('disable');
    upload_button_upload.classList.remove('loading');
  }
  upload_button_upload.addEventListener('click', async function () {
    if (changeDisable(this)) return;
    if (!_file) {
      alert('请您先选择要上传的文件~~');
      return;
    }
    changeDisable(true);
    // 生成文件的HASH名字
    let { filename } = await changeBuffer(_file);
    let formData = new FormData();
    formData.append('file', _file);
    formData.append('filename', filename);
    instance.post('/upload_single_name', formData).then(data => {
      if (+data.code == 0) {
        alert(`文件已经上传成功~~,您可以基于 ${data.servicePath} 访问这个资源~~`);
        return;
      }
      return Promise.reject(data.codeText)
    }).catch(reason => {
      alert('文件上传失败,请您稍后再试~~');
    }).finally(() => {
      changeDisable(false);
      upload_abbre.style.display = 'none';
      upload_abbre_img.src = '';
      _file = null;
    })
  })
})();

进度管控

<div class="item">
            <h3>单一文件上传「进度管控」</h3>
            <section class="upload_box" id="upload4">
                <input type="file" class="upload_inp">
                <div class="upload_button_box">
                    <button type="button"  class="upload_button select">上传文件</button>
                </div>
                <div class="upload_progress">
                    <div class="value"></div>
                </div>
            </section>
        </div>
(function () {
  let upload = document.querySelector('#upload4'),
    upload_inp = upload.querySelector('.upload_inp'),
    upload_button_select = upload.querySelector('.upload_button.select'),
    upload_progress = upload.querySelector('.upload_progress'),
    upload_progress_value = upload_progress.querySelector('.value');

  // 验证是否处于可操作性状态
  const checkIsDisable = element => {
    let classList = element.classList;
    return classList.contains('disable') || classList.contains('loading');
  }

  // 点击选择文件按钮,触发上传文件INPUT框选择文件的行为
  upload_button_select.addEventListener('click', function () {
    if (checkIsDisable(this)) return;
    upload_inp.click();
  })

  upload_inp.addEventListener('change', async function () {
    let file = upload_inp.files[0],
      data;
    if (!file) return;
    upload_button_select.classList.add('loading');
    try {
      let formData = new FormData();
      formData.append('file', file);
      formData.append('filename', file.name);
      data = await instance.post('/upload_single', formData, {
        // 文件上传中的回调函数 xhr.upload.onprogress
        onUploadProgress(ev) {
          let { loaded, total } = ev;
          upload_progress.style.display = 'block';
          upload_progress_value.style.width = `${loaded / total * 100}%`;
        }
      })
      if (+data.code === 0) {
        upload_progress_value.style.width = `100%`;
        await delay(300);
        alert(`恭喜您,文件上传成功,您可以基于 ${data.servicePath} 访问该文件~~`);
        return;
      }
      throw data.codeText;
    } catch (err) {
      alert('很遗憾,文件上传失败,请您稍后再试~~');
    } finally {
      upload_button_select.classList.remove('loading');
      upload_progress.style.display = 'none';
      upload_progress_value.style.width = `0%`;
    }
  })
})();

多文件上传

<div class="item">
         <h3>多文件上传</h3>
         <section class="upload_box" id="upload5">
             <input type="file" class="upload_inp" multiple>
             <div class="upload_button_box">
                 <button type="button" class="upload_button select">选择文件</button>
                 <button type="button"  class="upload_button upload">上传到服务器</button>
             </div>
             <ul class="upload_list">
                 <!-- <li key='xx'>
                     <span>文件:xxxxx</span>
                     <span><em>移除</em></span>
                 </li> -->
             </ul>
         </section>
     </div>
(function () {
let upload = document.querySelector('#upload5'),
 upload_inp = upload.querySelector('.upload_inp'),
 upload_button_select = upload.querySelector('.upload_button.select'),
 upload_button_upload = upload.querySelector('.upload_button.upload'),
 upload_list = upload.querySelector('.upload_list');
let _files = [];

// 点击选择文件按钮,触发上传文件INPUT框选择文件的行为
upload_button_select.addEventListener('click', function () {
 if (checkIsDisable(this)) return;
 upload_inp.click();
})

// 获取唯一值
const createRandom = () => {
 let ran = Math.random() * new Date();
 return ran.toString(16).replace('.', '');
};
upload_inp.addEventListener('change', async function () {
 _files = Array.from(upload_inp.files);
 if (_files.length === 0) return;
 // 我们重构集合的数据结构「给每一项设置一个位置值,作为自定义属性存储到元素上,后期点击删除按钮的时候,我们基于这个自定义属性获取唯一值,再到集合中根据这个唯一值,删除集合中这一项」
 _files = _files.map(file => {
   return {
     file,
     filename: file.name,
     key: createRandom()
   };
 });
 // 绑定数据
 let str = ``;
 _files.forEach((item, index) => {
   str += `<li key="${item.key}">
     <span>文件${index + 1}${item.filename}</span>
     <span><em>移除</em></span>
   </li>`;
 });
 upload_list.innerHTML = str;
 upload_list.style.display = 'block';
})

// 基于事件委托实现移除的操作
upload_list.addEventListener('click', ev => {
 let target = ev.target,
   curLi = null,
   key;
 if (target.tagName == 'EM') {
   curLi = target.parentNode.parentNode;
   if (!curLi) return;
   upload_list.removeChild(curLi);
   key = curLi.getAttribute('key');
   _files = _files.filter(item => item.key !== key);
   if (_files.length === 0) {
     upload_list.style.display = 'none';
   }
 }
})

// 验证是否处于可操作性状态
const checkIsDisable = element => {
 let classList = element.classList;
 return classList.contains('disable') || classList.contains('loading');
};

// 把文件上传到服务器
const changeDisable = flag => {
 if (flag) {
   upload_button_select.classList.add('disable');
   upload_button_upload.classList.add('loading');
   return;
 }
 upload_button_select.classList.remove('disable');
 upload_button_upload.classList.remove('loading');
};
upload_button_upload.addEventListener('click', async function () {
 if (checkIsDisable(this)) return;
 if (_files.length === 0) {
   alert('请您先选择要上传的文件~~');
   return;
 }
 changeDisable(true);
 // 循环发送请求
 let upload_list_arr = Array.from(upload_list.querySelectorAll('li'));
 _files = _files.map(item => {
   let fm = new FormData,
     curLi = upload_list_arr.find(liBox => liBox.getAttribute('key') === item.key),
     curSpan = curLi ? curLi.querySelector('span:nth-last-child(1)') : null;
   fm.append('file', item.file);
   fm.append('filename', item.filename);
   return instance.post('/upload_single', fm, {
     onUploadProgress(ev) {
       // 检测每一个的上传进度
       if (curSpan) {
         curSpan.innerHTML = `${(ev.loaded / ev.total * 100).toFixed(2)}%`;
       }
     }
   }).then(data => {
     if (+data.code === 0) {
       if (curSpan) {
         curSpan.innerHTML = `100%`;
       }
       return;
     }
     return Promise.reject();
   })
 })

 // 等待所有处理的结果
    const clearHandle = () => {
      changeDisable(false);
      _files = [];
      upload_list.innerHTML = '';
      upload_list.style.display = 'none';
    }

    Promise.all(_file).then(() => {
      setTimeout(() => {
        alert('恭喜您,所有文件都上传成功~~');
        clearHandle()
      }, 300);
    }).catch(() => {
      alert('很遗憾,上传过程中出现问题,请您稍后再试~~');
      clearHandle()
    }).finally(() => {
    })
})

})();

拖拽上传

<div class="item">
            <h3>拖拽上传</h3>
            <section class="upload_box" id="upload6">
                <input type="file" class="upload_inp">
                <div class="upload_drag">
                    <i class="icon"></i>
                    <span class="text">将文件拖到此处,或<a href="javascript:;" class="upload_submit">点击上传</a></span>
                </div>
                <div class="upload_mark">正在上传中,请稍等...</div>
            </section>
        </div>
(function () {
  let upload = document.querySelector('#upload6'),
    upload_inp = upload.querySelector('.upload_inp'),
    upload_submit = upload.querySelector('.upload_submit'),
    upload_mark = upload.querySelector('.upload_mark');
  let isRun = false;

  // 点击选择文件按钮,触发上传文件INPUT框选择文件的行为
  upload_submit.addEventListener('click', function () {
    upload_inp.click();
  });

  // 实现文件上传
  const uploadFile = async file => {
    if (isRun) return;
    isRun = true;
    upload_mark.style.display = 'block';
    try {
      let fm = new FormData,
        data;
      fm.append('file', file);
      fm.append('filename', file.name);
      data = await instance.post('/upload_single', fm);
      if (+data.code === 0) {
        alert(`恭喜您,文件上传成功,您可以基于 ${data.servicePath} 访问该文件~~`);
        return;
      }
      throw data.codeText;
    } catch (err) {
      alert(`很遗憾,文件上传失败,请您稍后再试~~`);
    } finally {
      upload_mark.style.display = 'none';
      isRun = false;
    }
  };

  // 手动选择
  upload_inp.addEventListener('change', function () {
    let file = upload_inp.files[0];
    if (!file) return;
    uploadFile(file);
  });

  // 拖拽获取 dragenter dragleave dragover drop
  /*   upload.addEventListener('dragenter', function () {
      ev.preventDefault();
      console.log('进入');
    });
    upload.addEventListener('dragleave', function () {
      ev.preventDefault();
      console.log('离开');
    }); */
  upload.addEventListener('dragover', function (ev) {
    ev.preventDefault();
  });
  upload.addEventListener('drop', function (ev) {
    ev.preventDefault();
    let file = ev.dataTransfer.files[0];
    if (!file) return;
    uploadFile(file);
  });
})();

大文件上传

<div class="container">
        <div class="item">
            <h3>大文件上传</h3>
            <section class="upload_box" id="upload7">
                <input type="file" class="upload_inp">
                <div class="upload_button_box">
                    <button class="upload_button select">上传图片</button>
                </div>
                <div class="upload_progress">
                    <div class="value"></div>
                </div>
            </section>
        </div>
    </div>
(function () {
  let upload = document.querySelector('#upload7'),
    upload_inp = upload.querySelector('.upload_inp'),
    upload_button_select = upload.querySelector('.upload_button.select'),
    upload_progress = upload.querySelector('.upload_progress'),
    upload_progress_value = upload_progress.querySelector('.value');

  const checkIsDisable = element => {
    let classList = element.classList;
    return classList.contains('disable') || classList.contains('loading');
  };

  const changeBuffer = file => {
    return new Promise(resolve => {
      let fileReader = new FileReader();
      fileReader.readAsArrayBuffer(file);
      fileReader.onload = ev => {
        let buffer = ev.target.result,
          spark = new SparkMD5.ArrayBuffer(),
          HASH,
          suffix;
        spark.append(buffer);
        HASH = spark.end();
        suffix = /\.([a-zA-Z0-9]+)$/.exec(file.name)[1];
        resolve({
          buffer,
          HASH,
          suffix,
          filename: `${HASH}.${suffix}`
        });
      };
    });
  };

  // 点击选择文件按钮,触发上传文件INPUT框选择文件的行为
  upload_button_select.addEventListener('click', function () {
    if (checkIsDisable(this)) return;
    upload_inp.click();
  });

  // 文件上传
  upload_inp.addEventListener('change', async function () {
    let file = upload_inp.files[0];
    if (!file) return;
    upload_button_select.classList.add('loading');
    upload_progress.style.display = 'block';

    // 获取文件的HASH
    let already = {},
      data = null,
      {
        HASH,
        suffix
      } = await changeBuffer(file);

    // 获取已经上传的切片信息
    try {
      data = await instance.get('/upload_already', {
        params: {
          HASH
        }
      });
      if (+data.code === 0) {
        already = data.fileList;
      }
    } catch (err) { }

    // 实现文件切片处理 「固定数量 & 固定大小」
    let max = 1024 * 100,
      count = Math.ceil(file.size / max),
      index = 0,
      chunks = [];
    if (count > 100) {
      max = file.size / 100;
      count = 100;
    }
    while (index < count) {
      chunks.push({
        file: file.slice(index * max, (index + 1) * max),
        filename: `${HASH}_${index + 1}.${suffix}`
      })
      index++;
    }

    // 上传成功的处理
    index = 0;
    const clear = () => {
      upload_button_select.classList.remove('loading');
      upload_progress.style.display = 'none';
      upload_progress_value.style.width = '0%';
    };

    const complete = async () => {
      // 管控进度条
      index++;
      upload_progress_value.style.width = `${index / count * 100}%`;
      // 当所有切片都上传成功,我们合并切片
      if (index < count) return;
      upload_progress_value.style.width = `100%`;
      try {
        data = await instance.post('/upload_merge', {
          HASH,
          count
        }, {
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded'
          }
        });
        if (+data.code === 0) {
          await delay(300)
          alert(`恭喜您,文件上传成功,您可以基于 ${data.servicePath} 访问该文件~~`);
          clear();
          return;
        }
        throw data.codeText;
      } catch (error) {
        alert('切片合并失败,请您稍后再试~~');
        clear();
      }
    }

    // 把每一个切片都上传到服务器上
    chunks.forEach(chunk => {
      // 已经上传的无需在上传
      if (already.length > 0 && already.includes(chunk.filename)) {
        complete();
        return;
      }
      let fm = new FormData;
      fm.append('file', chunk.file);
      fm.append('filename', chunk.filename);
      instance.post('/upload_chunk', fm).then(data => {
        if (+data.code === 0) {
          complete();
          return;
        }
        return Promise.reject(data.codeText);
      }).catch(() => {
        alert('当前切片上传失败,请您稍后再试~~');
        clear();
      });
    })
  })
})();

Utils

(function () {
"use strict";
/* 检测数据类型的 */
const getProto = Object.getPrototypeOf,
  class2type = {},
  toString = class2type.toString,
  hasOwn = class2type.hasOwnProperty;
const isFunction = function isFunction(obj) {
  return typeof obj === "function" && typeof obj.nodeType !== "number" &&
      typeof obj.item !== "function";
};
const isWindow = function isWindow(obj) {
  return obj != null && obj === obj.window;
};
const toType = function toType(obj) {
  let reg = /^\[object (.+)\]$/;
  if (obj == null) return obj + "";
  return typeof obj === "object" || typeof obj === "function" ?
      reg.exec(toString.call(obj))[1].toLowerCase() :
      typeof obj;
};
const isPlainObject = function isPlainObject(obj) {
  let proto, Ctor;
  if (!obj || toString.call(obj) !== "[object Object]") return false;
  proto = getProto(obj);
  if (!proto) return true;
  Ctor = hasOwn.call(proto, "constructor") && proto.constructor;
  return typeof Ctor === "function" && Ctor === Object;
};
const isEmptyObject = function isEmptyObject(obj) {
  if (obj == null || !/^(object|function)$/.test(typeof obj)) return false;
  let keys = Object.getOwnPropertyNames(obj);
  if (typeof Symbol !== "undefined") keys = keys.concat(Object.getOwnPropertySymbols(obj));
  return keys.length === 0;
};
const isArrayLike = function isArrayLike(obj) {
  let length = !!obj && "length" in obj && obj.length,
      type = toType(obj);
  if (isFunction(obj) || isWindow(obj)) return false;
  return type === "array" || length === 0 ||
      typeof length === "number" && length > 0 && (length - 1) in obj;
};

// 检测是不是有效数字
const isNumeric = function isNumeric(obj) {
let type = typeof (obj);
return (type === "number") || type === "string" && !isNaN(obj)
}

/* 函数防抖&节流 */
const clearTimer = function clearTimer(timer) {
  if (timer !== null) clearTimeout(timer);
  return null;
};
const debounce = function debounce(func, wait, immediate) {
  if (typeof func !== "function") throw new TypeError("func is not a function!");
  if (typeof wait === "boolean") immediate = wait;
  if (typeof wait !== "number") wait = 300;
  if (typeof immediate !== "boolean") immediate = false;
  let timer = null;
  return function operate(...params) {
      let now = !timer && immediate;
      timer = clearTimer(timer);
      timer = setTimeout(() => {
          timer = clearTimer(timer);
          if (!immediate) func.apply(this, params);
      }, wait);
      if (now) return func.apply(this, params);
  };
};
const throttle = function throttle(func, wait) {
  if (typeof func !== "function") throw new TypeError("func is not a function!");
  if (typeof wait !== "number") wait = 300;
  let timer = null,
      previous = 0;
  return function operate(...params) {
      let now = +new Date(),
         remaining = wait - (now - pervious);
      if (remaining <= 0) {
          timer = clearTimer(timer);
          previous = +new Date();
          return func.apply(this, params);
      }
      if (!timer) {
          timer = setTimeout(() => {
              timer = clearTimer(timer);
              previous = +new Date();
              func.apply(this, params);
          }, remaining);
      }
  };
};

/* 迭代方法:迭代数组、类数组、对象 */
const each = function each(obj, callback) {
  if (obj == null || !/^object$/.test(typeof obj)) throw new TypeError("obj must be an object/array/likeArray");
  if (typeof callback !== "function") throw new TypeError("callback is not a function");
  let item, keys, key;
  if (isArrayLike(obj)) {
      for (let i = 0; i < obj.length; i++) {
          item = obj[i];
          if (callback.call(item, item, i) === false) break;
      }
  } else {
      keys = Object.getOwnPropertyNames(obj);
      if (typeof Symbol !== "undefined") keys = keys.concat(Object.getOwnPropertySymbols(obj));
      for (let i = 0; i < keys.length; i++) {
          key = keys[i];
          item = obj[key];
          if (callback.call(item, item, key) === false) break;
      }
  }
  return obj;
};

/* 数组/对象的深浅拷贝&深浅合并 */
const clone = function clone(obj, deep, exist) {
  if (obj == null) return obj;
  if (typeof deep !== "boolean") deep = false;
  let ctor = obj.constructor,
      type = toType(obj),
      isArray = Array.isArray(obj),
      isObject = isPlainObject(obj),
      result;
  if (/^(regexp|date)$/i.test(type)) return new ctor(obj);
  if (/^(error)$/i.test(type)) return new ctor(obj.message);
  if (typeof obj === "function") {
      return function (...params) {
          return obj.call(this, ...params);
      };
  }
  if (!isArray && !isObject) return obj;
  if (!Array.isArray(exist)) exist = [];
  if (exist.indexOf(obj) > -1) return obj;
  exist.push(obj);
  result = new ctor();
  each(obj, (value, key) => {
      if (deep) {
          result[key] = clone(value, deep, exist);
          return;
      }
      result[key] = value;
  });
  return result;
};
const merge = function merge() {
  let options,
      target = arguments[0] || {},
      i = 1,
      length = arguments.length,
      exist = arguments[length - 1],
      deep = false;
  if (typeof target === "boolean") {
      deep = target;
      target = arguments[i] || {};
      i++;
  }
  if (target == null || (typeof target !== "object" && !isFunction(target))) target = {};
  Array.isArray(exist) && exist.isExist ? length-- : (exist = [], exist.isExist = true);
  for (; i < length; i++) {
      options = arguments[i];
      if (options == null) continue;
      if (exist.indexOf(options) > -1) return options;
      exist.push(options);
      each(options, (copy, name) => {
          let copyIsArray = Array.isArray(copy),
              copyIsObject = isPlainObject(copy),
              src = target[name];
          if (deep && copy && (copyIsArray || copyIsObject)) {
              if (copyIsArray && !Array.isArray(src)) src = [];
              if (copyIsObject && !isPlainObject(src)) src = {};
              target[name] = merge(deep, src, copy, exist);
          } else if (copy !== undefined) {
              target[name] = copy;
          }
      });
  }
  return target;
};

/* 暴露API */
const utils = {
  version: '1.0.0',
  debounce,
  throttle,
  isFunction,
  isWindow,
  toType,
  isPlainObject,
  isEmptyObject,
  isArrayLike,
  isNumeric,
  each,
  clone,
  merge
};
if (typeof module === "object" && typeof module.exports === "object") module.exports = utils;
if (typeof window !== "undefined") window.utils = utils;
})();