前端系统课程 - 37. 给简历加数据库

使用 LeanCloud 服务

  • 创建一个应用,选择需要的服务;

  • 引入 JS 文件,得到 window.AV 对象;

  • 初始化 AV 对象(代码直接拷);

  • 新建一条数据(代码直接拷)。

如何看文档

  • CRM 方法,先抄(Copy),然后运行(Run),一步步修改(Modify)为自己想要的代码。

  • 善于使用页面内的搜索功能,要学会提炼关键字。

MVC 之 M

数据模型,也就是 Model 层;简单来说,就是将需要获取和保存等操作数据的相关代码,也封装为一个对象,就像 View 层和 Controller 层那样。

MVC 到底是什么

  • 它是一种代码组织的思想和形式;将实现一个或一组功能的代码,通过不同的功能分为数据层(Model)、视图层(View)和控制层(Controller)。

  • 数据层用来处理要操作的各种数据。

  • 视图层用来监听并设置页面上与功能相关的 DOM 元素。

  • 控制层通过数据层的数据来更新视图层,通过视图层来告诉数据层如何操作数据。

  • 它们之间的运作流程是:视图层发生改变后,控制层会监听到这些变化,并将改变后的操作传递到数据层,数据层通过控制层的命令来进行数据发送或获取新数据,再将这些操作完成的数据传递给控制层,控制层通过这些数据,来更新视图层。

  • 以上过程可以看出,控制层是连接视图层和数据层的桥梁,而视图层和数据层都是比较纯净并且毫无交集的。

课后拾遗

  • Promise 对象的第一次 then() 方法中的的两个函数,分别是请求成功和请求失败后的回调函数;而如果有第二次 then() 方法调用,那么第二次的两个函数,分别是上一层成功回调和上一层两个都失败后的回调函数。

前端系统课程 - 36. MVC 之 VC

关于立即执行函数

  • 由于全局变量容易出错,所以要减少全局变量的使用,尽量使用局部变量;而 ES5 中只有函数才有局部变量。

  • 具名函数一般也都是全局变量,所以要使用匿名函数,但是要调用匿名函数就要使其成为表达式,例如前面加 ! 取反操作符操作符等方法。

使用闭包

通过闭包可以使变量不暴露,却可以在函数外部使用,也是一种减少全局变量的常用方式,例如:

1
2
3
4
5
6
7
8
9
10
function person() {
var age = 18;
return function () {
return ++age;
};
}

var setAge = person();
console.log(setAge()); // 19
console.log(setAge()); // 20

视图和控制

  • 将不同功能的代码分成不同的独立文件,就是模块化的一种方式。

  • 在模块化的基础上,将要操作的 DOM 元素独立为一个对象(View),将这个对象以参数形式传入到控制视图变化的函数中,再将这些控制函数独立为一个对象(Controller)的属性;这样就把操作视图的代码和视图对象分隔开了。

前端系统课程 - 35. 自己实现 AJAX

AJAX 设置请求报文

  • AJAX 对象 open() 方法的第一个参数对应请求报文第一部分中的请求方式,第二个参数则包含了第一部分中的路径和协议,同时包含第二部分中的 Host 内容。

  • 使用 setRequestHeader() 方法可以设置请求报文中的第二部分中的内容,这个方法需要在 open()send() 之间使用;接收两个字符串类型的参数,分别是第二部分内容的 key 和 value;例如:setRequestHeader('Content-Type', 'x-www-form-urlencoded')。第二部分的有些内容是禁止修改的,例如浏览器(用户)信息。

  • 请求报文的第四部分也就是请求体,则需要使用 send() 方法设置,它接收一个字符串类型的参数,对应的就是请求报文体的内容;要注意请求报文体使用 GET 方法发送时,是不会展示的。

AJAX 获取响应报文

  • 响应报文由服务端设置并发送,而用 AJAX 可以获取响应报文;获取第一部分中的状态码就是响应成功后 status 属性的值,状态信息则可以通过 statusText 属性来获取。

  • 使用 AJAX 对象的 getAllResponseHeaders() 方法可以获取响应报文第二部分整个内容,返回值是字符串;如果要获取某一个内容的值,可以使用 getResponseHeader() 方法,将要获取内容的 key 传入即可。

  • 响应报文的第四报文就是我们最需要的数据,通过 responseText 属性获取即可。

封装 AJAX

  • 如果参数较多,为了使参数更加直观,函数接收参数可以用对象的形式,每个参数就是这个对象的一个属性,属性名可以很好的将参数的功能标示并区分开,这种方式叫做“参数命名”。

  • 通过判断函数参数列表的长度或者每个参数的类型,可以让函数接收不同个数或者不同类型的参数,进而做出不同的处理,这就是 JavaScript 中的函数重载。

  • 简易封装代码示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    function ajax(options) {
    let ajax,
    method = options.method || 'GET',
    url = options.url,
    async = options.async || true,
    headers = options.headers,
    body = options.body,
    success = options.success,
    fail = options.fail;

    if (window.XMLHttpRequest) {
    ajax = new XMLHttpRequest();
    } else {
    ajax = new ActiveXObject('Microsoft.XMLHTTP');
    }

    ajax.open(method, url, async);

    for (let key in headers) {
    if (headers.hasOwnProperty(key)) {
    ajax.setRequestHeader(key, headers[key]);
    }
    }

    ajax.addEventListener('readystatechange', () => {
    if (ajax.readyState === 4) {
    if (ajax.status === 200) {
    console.log('Success!');
    success.call(undefined, responseText);
    } else {
    console.log('Fail……');
    fail && fail.call(undefined, ajax);
    }
    }
    });

    ajax.send(body);
    }

ES6 中的解构赋值

前面的封装函数中的参数,除了 AJAX 对象,剩下的都是来自 options 这个对象参数;在 ES6 中,如果参数和对象中属性顺序相同,可以直接写作 {method, url, async, headers, body, success, fail} = options,或者直接将代码块放入参数即可,这是解构赋值的一种应用;还有一个比较常见的应用,例如两个变量交换:

1
2
3
4
5
let a = 1;
let b = 2;
[a, b] = [b, a];
console.log(a); // 2
console.log(b); // 1

这种语法在一些语言中很早就实现了。

ES6 中的 Promise

  • Promise() 是一个全局方法,它可以看做是一种确定函数形式的规范。

  • 在 jQuery 的 AJAX 方法调用后,再使用 then() 方法就可以运行成功或失败的回调函数,成功是第一个参数,失败是第二个参数。

  • 使用 then() 方法运行的回调函数还可以将操作结果返回,以供下一个 then() 方法使用,这样就可以多结果进行多次处理了,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    $.ajax(
    ...somcode
    ).then(
    (responseText) => {
    console.log('第一次处理的' + responseText);
    return responseText;
    },
    (error) => {
    console.log(error);
    return error;
    }
    ).then(
    responseText => console.log('上次处理后的' + responseText),
    error => console.log(error)
    );
  • then() 方法是 Promise 实例的方法,创建 Promise 实例可以在 AJAX 函数内,直接返回一个实例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function ajax(options) {
    return new Promise((resolve, reject) => {
    let ajax
    ...
    if (ajax.readyState === 4) {
    if (ajax.status === 200) {
    resolve.call(undefined, responseText); // 成功则调用resolve
    } else {
    reject.call(undefined, ajax); // 失败则调用reject
    }
    }
    ...
    });
    }

前端系统课程 - 34. AJAX 是什么鬼

如何发请求

我们知道有一些标签可以发送请求:

  • <form> 表单可以发请求,但是会刷新页面或新开页面。

  • <a> 标签可以发 GET 请求,但是也会刷新页面或新开页面。

  • <img> 标签可以发 GET 请求,但是只能以图片的形式展示。

  • <link> 标签可以发 GET 请求,但是只能以 CSS、favicon 的形式展示。

  • <script> 可以发 GET 请求,但是只能以脚本的形式运行。

但我们想要一种方式可以实现以下功能:

  • 可以发送 GET、POST、PUT、DELETE 等不同类型的请求。

  • 想以什么形式展示就以什么形式展示。

微软的突破

微软在 IE5 率先在 JavaScript 中引入 ActiveX 对象(API),使得 JavaScript 可以直接发起 HTTP 请求。随后,其他浏览器也跟进创造了一个类似的对象,取名为 XMLHttpRequest,并被纳入了 W3C 规范。

什么是 AJAX

Jesse James Garrett 将如下技术取名为 AJAX(Asynchronous JavaScript and XML,异步 JavaScript 和 XML):

  • 使用 XMLHttpRequest 对象发送请求;

  • 服务器返回 XML 格式的字符串;

  • JavaScript 解析 XML,并更新局部页面。

用原生 JavaScript 发送 AJAX 请求

  • 首先通过 new 操作符和 XMLHttpRequest() 构造器创建一个实例对象;

  • 创建好实例对象后,就可以通过此对象的 open() 方法发送 HTTP 请求;这个方法接收五个参数,一般要了解的是前三个:

    • 第一个参数是字符串,代表请求类型,例如 GET 或 POST 等;

    • 第二个参数也是字符串,表示请求的 URL 地址或路径;

    • 第三个不常用但是比较重要,参数类型是布尔值,true 表示异步传输,false 表示同步传输。

  • 然后给这个对象绑定监听 onreadystatechange 事件,这个事件称为“通信状态改变事件”,它监听的其实是 readyState 属性的变化,这个属性用五个数字代表五种状态:

    • 0:UNSENT,代理被创建,但尚未调用 open() 方法。

    • 1:OPENED,open() 方法已经被调用。

    • 2:HEADERS_RECEIVED,send() 方法已经被调用,并且头部和状态已经可获得。

    • 3:LOADING,下载中,responseText 属性已经包含部分数据。

    • 4:DONE,下载操作已完成。

      从前面的列表中可以看出,只要判断 readyState 属性值为 4 时,就说明请求通信成功。

  • 请求成功后,还要判断返回的状态码,也就是 status 的属性值,如果状态码是 200 证明响应成功了。

  • 响应成功后,便可以得到响应返回的数据,这些数据就是 responseText 的属性值,拿到这个值就可以做相应的数据处理。

  • 用 AJAX 发送请求就像打手机,先解锁手机(创建对象),然后输入号码(初始化),再拨通这个号码(发送请求),最后等待接通后通话(等待响应及数据处理)。

  • 一个基础的 AJAX 方法示例代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    function ajax(method, url, async, fnSuccess, fnFail) {
    let ajax;
    // 创建对象
    if (window.XMLHttpRequest) {
    ajax = new XMLHttpRequest();
    } else {
    ajax = new ActiveXObject('Microsoft.XMLHTTP');
    }
    // 初始化
    ajax.open(method, url, async);
    // 发送请求
    ajax.send();
    // 接收请求
    ajax.onreadystatechange = () => {
    if (ajax.readyState === 4) {
    if (ajax.status === 200) {
    console.log('成功!');
    fnSuccess(ajax.responseText);
    } else {
    console.log('失败!');
    fnFail && fnFail();
    }
    }
    };
    }

JSON 与 JavaScript

  • JSON 是“抄袭” JavaScript 部分语法后发明的另一种语言。

  • JSON 中的数据类型和 JavaScript 中的数据类型非常相似,但区别是 JSON 中没有 Undefined 和 Function,并且在 JSON 中,字符串必须使用双引号包裹。

  • JSON 只是一种数据交换格式,它没有原型链,所以无法传输复杂的数据,例如引用数据类型。

  • HTTP 请求返回的数据都是以字符串的形式返回,只不过这些字符串可以符合不同的格式,比如 XML 或 JSON。

  • 服务器返回的 JSON 格式字符串,可以使用浏览器提供的 window.JSON.parse() 方法将其转换为对应的 JavaScript 数据,例如对象,或数组等等。

同源策略

  • 为什么 form 表单提交没有跨域问题,而 AJAX 提交却有跨域问题呢?

    这是因为原页面用 form 表单提交到另一个域名后,会跳转到新页面,原页面的脚本无法获取新页面中的内容,这在浏览器认为是安全的。

    而 AJAX 是可以读取响应内容的,并且不会跳转新页面,所以浏览器不允许 AJAX 被响应;用 AJAX 是可以成功地将请求发送出去的,只是接收不到服务器的响应。

  • 前面的问题其实体现的是浏览器的同源策略;同源策略的含义是:A 网页设置的 Cookie,B 网页不能打开,除非两个网页“同源”,同源指的是“三个相同”,即协议相同域名相同端口相同

  • 如果两个网页非同源,那么三种行为会受到限制:

    • Cookie、LocalStorage 和 IndexDB 无法读取。

    • DOM 无法获得。

    • AJAX 请求发送后无法得到响应。

CORS 跨域

如果要突破同源策略,可以使用 CORS 机制来进行跨域。

CORS(Cross-Origin Resource Sharing,跨源资源共享),是一种允许当前域的资源(比如 HTML、JS 等)被其他域的脚本请求访问的机制。

一般要实现 CORS,需要后端为指定共享的 URL 添加一个响应头,例如:response.setHeader('Access-Control-Allow-Origin', url);如果允许所有的 URL,可以将参数传入一个通配符字符串 '*'

前端系统课程 - 33. JSONP 是什么鬼

数据库

  • 能长久存储数据的仓库,就是数据库,文件系统也可以看作一种”数据库“。

  • 网页上有些数据不能写死,需要从数据库读取后再显示到页面上;而这些数据前端修改后,还要通知后端作出相同修改,这时便涉及到了前后端数据通信问题。

前后端通信

  • 使用 <form> 标签组成的 form 表单可以向后台提交数据,而且可以设置提交方式、提交地址以及提交成功页面的打开位置等等。

  • 使用 form 表单提交数据请求,后端接收到请求后对数据库进行相应的操作,这是最初前后端通信的方式;这种方式每次完成操作后,前端需要的到后端反馈信息后,再刷新页面才能看到最新的数据。

  • 后来的开发者将后端的反馈信息引导至一个内嵌的 iframe 窗口,这样可以让反馈信息在当前页面显示了,一定程度上提升了用户体验,但是最新的数据,还是得刷新整个页面后才能获取到。

  • 在 HTML 中,有些标签具备不需要用户操作就可以发送请求的功能(<a> 标签是需要用户操作的),例如 <link><script><img> 等标签,但它们只能发送 GET 请求,无法发送 POST 请求。

  • <img><script> 等标签都可以监听其 onloadonerror 事件,来判断加载成功和失败,图片验证虽然需要返回图片文件(光返回 Content-Type: image/xxx 是不行的),判断繁琐,但是总算可以向服务器提交数据了;当请求成功后,前端可以自动为用户刷新页面,减少用户的操作,也可以在收到成功反馈后,直接修改页面数据,这样在不刷新的情况下也能看到数据变化。

  • <script> 标签也可以发送请求,也不必返回图片,但是必须将它放到页面当中才可以,因为 <script> 中的内容需要解析并且使用的是 src 属性引用的外部文件,才会发送请求去获取这个文件。由于每次发送请求都会动态地创建一个 <script> 标签,所以每次请求成功后,文档中都会出现一个 <script> 标签;这个插入到页面中的 <script> 标签如果含有脚本代码,就会被执行;利用这些特点,可以不必监听 onload 事件,直接在后端返回的文件中,写一段代码,后端操作数据成功并将这个文件返回到前端,页面就会执行这段代码的内容了,这段代码可以做很多事,比如可以直接将页面的相应数据修改;页面中增加的 <script> 标签在添加并加载完成后,便删除它,这样在请求操作完成后,文档中也不会有冗余的”副产品”了。前面描述的这种方案是 AJAX 出现之前,局部更新页面内容的常见方案,称为“SRJ(Server Rendered JavaScript)”方案。

什么是 JSONP

  • 由于 <script> 标签发送的请求是不受域名限制的,也就是说,它可以跨域访问。利用这个特性,不同的网站(域名)之间也可以进行通信。

  • 如果不同的域名之间通信时,后端的返回的代码中写的是对具体页面数据的操作(数据耦合度高),那么就需要后端对页面中的细节非常了解,这显然是非常困难的;要对这些数据进行解耦,可以让后端在操作成功后,返回的数据中调用指定的方法,传入指定的参数;再进一步,前后端约定指定的查询参数,前端将要运行的方法名通过查询参数的形式放在 src 属性的地址中,传送给后端,后端截取约定的名称,传入参数后返回给前端,这样就顺利解耦了。

  • 前面的方法中,后端可以通过给指定的方法传参来向前端传送数据,那么这个数据换成是 JSON 数据也没有问题,这就是 JSONP。

  • 在使用 JSONP 时,有一些约定俗成的命名:查询参数的名称为 callback,而回调函数的名称内要包含随机数,并且每次请求后都会删除,这样不会污染全局变量,也不会发生命名冲突;

  • 在 jQuery 中的 AJAX 对象也集成了 JSONP 请求方式(虽然 JSONP 和 AJAX 没有关系),例如:

    1
    2
    3
    4
    5
    $.ajax({
    url: 'http://www.test.com:8811/example',
    dataType: 'jsonp',
    success: response => console.log(response);
    });
  • JSONP 的原理总结:

    • 浏览器要发送请求时,动态地创建一个 <script> 标签,并将其 src 属性中的地址指向目标服务器,同时在地址中传入一个包含已在本地声明的回调函数名称的查询参数(一般为 callback),将这个 <script> 插入到文档中,触发一个 GET 请求;

    • 服务器收到请求后,根据查询参数中的回调函数名称,构造一个调用回调函数的响应,并将数据通过回调函数的参数传入后发送给浏览器;

    • 浏览器接收到服务器的响应,执行响应中带有参数的回调函数,之后便通过参数获取到了服务器返回的数据;示例代码:

  • 示例代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    let btn = document.querySelector('#btn'),
    fnName = `res${parseInt(Math.random() * 100000, 10)}`;

    window[fnName] = response => console.log(response);

    btn.addEventListener('click', () => {
    let script = document.createElement('script');
    script.src = `http://frank.com:8001?callback=${fnName}`;
    script.addEventListener('load', e => e.currentTarget.remove());
    script.addEventListener('error', (e) => {
    console.log('fail!');
    });

    document.body.appendChild(script);
    });
  • 由于 <script> 标签不具备发送 POST 请求的能力,所以 JSONP 方法不能发送 POST 请求;另外,JSONP 与 JSON 数据没多大关系,只是具备接收 JSON 数据的能力;而 JSONP 与 AJAX 也没有什么关系,只是被 jQuery 集成到了其 AJAX 对象中。

前端系统课程 - 32. DOM 事件

DOM 事件模型

  • e.stopPropagation() 方法可以阻止事件冒泡。

  • e.preventDefault() 方法可以阻止元素的默认事件。

  • 阻止默认事件可能会带来一些意外的 bug,例如复选框不能选中等等。

  • 如果想让 jQuery 中的 on() 方法同时阻止默认事件和阻止冒泡,可以直接在回调函数参数后,再传一个 false 参数。

  • 为元素绑定事件监听是需要占用内存的,所以一个监听事件完成后,在非必须的情况下,尽量把这个事件监听再解除掉;在 jQuery 中,善用一次性事件监听 one() 方法。

  • 如果在一个事件监听的回调函数中,又对其上级元素绑定了事件监听,那么这个后绑定的事件监听,在前一个事件触发后会立即完成绑定,并在本次的事件触发过程中生效。例如:

    1
    2
    3
    4
    5
    6
    $('#son').on('click', () => {
    console.log('子元素被点击了');
    $('#father').one('click', () => {
    console.log('父元素被点击了'); // 本来觉得这里不会执行,但确实会执行;
    });
    });

    如果不想让父元素事件监听的回调函数执行,最好的办法就是在子元素添加阻止冒泡;也可以使用延时定时器延迟对父元素事件监听的绑定,即使定时器设置为 0 秒,也会让绑定动作在事件触发完成以后执行,但只能延迟一轮,再次点击子元素,父元素由于绑定已经在上一次点击完成了,所以还会触发。

优化无缝轮播

  • 先前做的无缝轮播有个 bug,如果切换到别的页面一段时间后再切回来,轮播动画会出错(表现为多个动画同时执行),这是因为浏览器为了节省资源,会降低非展示状态的页面中的定时器运行频率。

  • visibilitychange 事件,称为“标签可见性改变事件”;这个事件可以知道用户有没有在看自己,如果用户切换了标签或页面,那么 document.hidden 属性便为 true,反之则反。

  • 通过判断页面的 document.hidden 属性值,来随时控制定时器的关闭和开启,就可以解决前面的轮播动画问题了。

  • 通过切换 class 属性的方式做可切换的无缝轮播非常困难,所以要换一种方式;当实现一个功能发现走到死胡同时,要勇于试错;

  • 用到的 jQuery API:

    • clone() 方法,复制一个元素,传入 true 表示同时复制其子元素,这与DOM API 的 cloneNode() 方法类似。

    • append() 方法,将一个元素插入到目标元素的末尾。

    • prepend() 方法,将一个元素插入到目标元素的首位。

    • offset() 方法,获取或设置元素当前的偏移位置;由于获取时要计算样式,所以会暂时阻断 CSS 解析,可以利用这点来防止与后面准备设置的 CSS 内容合并。

    • on() 方法还可以通过父元素为所有子元素绑定事件,通过选择器来指定子元素,称为“事件委托”,例如:

      $('father').on('click', '.son', function(){ ...somecode });

    • innerWidth() 方法可以获取元素边框以内的宽度,innerHeight() 同理。

    • is() 方法可以查看元素是否匹配选择器;例如使用此方法判断元素的隐藏状态,并切换元素的隐藏状态(可替代废弃的 toggle() 方法):

      $('#test').on('click', () => $('.div').is(':hidden') ? $('.div').show() : $('div').hide());

课后拾遗

  • 在 jQuery 中,为元素添加自定义属性,要么通过下标 0,要么使用 attr() 方法,必须将属性添加到 DOM 元素上,特别是动画中的定时器对象。

  • 再次理解动画原理,元素的位置(或其他可用属性)在指定时间间隔,逐渐改变,直到达到设定的目标位置;体现到属性值上,属性值不停的累加,每次累加计算好的数量,直到此属性值到达设定的目标值。

  • 如果需要一个开关来表示是否到达目标值,直接判断当时值和目标值是否相等,把结果赋值给开关变量,无需进行判断;例如:let ok = current === target

  • 在 jQuery 中,visibility: hiddenopacity: 0 都被视作可见的,因为它们在页面上占据了相应的物理空间。

前端系统课程 - 31. 阶段考试及相关知识

题目 01

请写出一个符合 W3C 规范的 HTML 文件,要求:

  1. 页面标题为“我的页面”;

  2. 页面中引入一个外部 CSS 文件,文件路径为 /style.css

  3. 页面中引入一个外部 CSS 文件,路径为 /print.css,该文件仅在打印时生效;

  4. 页面中引入一个外部 CSS 文件,路径为 /mobile.css,该文件仅在设备宽度小于 500 像素时生效;

  5. 页面中引入一个外部 JS 文件,路径为 /main.js

  6. 页面中引入一个外部 JS 文件,路径为 /gbk.js,文件编码为 GBK;

  7. 页面中添加一个 <svg> 标签,里面有一个直径为 100 像素的圆圈,颜色随意;

  8. 注意题目中的路径。


回答:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html lang="zh_Hans">
<head>
<meta charset="UTF-8">
<title>我的页面</title>
<link rel="stylesheet" href="./style.css">
<link rel="stylesheet" href="./print.css" media="print">
<link rel="stylesheet" href="./mobile.css" media="(max-width: 500px)">
</head>
<body>
<svg version="1.1"
baseProfile="full"
width="300" height="200"
xmlns="http://www.w3.org/2000/svg">
<circle cx="100" cy="100" r="50" stroke="red" fill="transparent"></circle>
</svg>
<script src="./main.js"></script>
<script src="./gbk.js" charset="GBK"></script>
</body>
</html>

补充:

大部分是扩展知识,例如媒体查询的设置,<svg> 标签的基本用法,都能很轻松的查阅到;另外给出的文件路径不能直接使用,不然就代表根目录了,一般不会这样存放资源;要么去除斜杆,要么直接在斜杠前加个点,为了保险起见,还是加了个点,表示同一层级的目录下。

题目 02

2016年腾讯前端面试题:移动端是怎么做适配的?

回答要点:

  1. <meta name="viewport">

  2. 媒体查询;

  3. 动态 rem 方案。


回答:

  • 一般要将页面宽度适配为设备宽度,除了使用 flex 布局和新的宽度单位 vw / vh 以外,在 HTML 文件的 <head> 标签中进行视口设置也是必要的,内容如下:

    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">

    content 属性中的内容含义依次是:页面宽度等于设备宽度,用户缩放禁用,初识缩放比例 1.0 倍,最大缩放比例 1.0 倍,最小缩放比例 1.0 倍。

  • 如果还想做 PC 端页面的响应式适配,也就是让页面在不同的分辨率拥有不同的样式,除了类似 Bootstrap 中的“十二栅格”,还有就是媒体查询了,可以使用 CSS 中的媒体查询功能,来匹配不同宽度页面下的样式,例如:

    1
    2
    3
    4
    5
    @media all and(max-width: 768px) { /* 以下样式在所有设备中最大支持到宽度为768px,超过便失效 */
    body {
    font: bold 12px/1.6 '微软雅黑';
    }
    }

    媒体查询是非常好用的,但是缺点是如果支持的分辨率太多,那就需要写很多的样式才行,比较麻烦。

  • 如果是以小屏设备(主要是手机)为主,但是手机分辨率太多,这时可以使用 em 单位或 rem 单位。这两个长度单位会根据设置好的与像素的比例,在不同的页面宽度中,保持相同比例的像素大小。em 单位比较繁琐,因为它的大小是基于父元素文字大小的,如果层级过多,将会涉及大量的计算,一般不推荐使用;而 rem 的大小,是基于根元素的文字大小,这就比较统一且好算,一般将首要的支持设备页面中(或设计图的大小),根元素文字大小设置为 100px 是最好用的了,不设置为 10px 可以避免很多麻烦哟!

    那么如何保证不同宽度设备下页面根元素的像素比例呢?最好还是用 JS 方法实现,在页面加载完和窗口宽度改变时更新根元素上的文字大小即可,以安卓手机的设计图宽度,示例如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    !function (n) {
    var e = n.document,
    t = e.documentElement,
    o = "orientationchange" in n ? "orientationchange" : "resize",
    a = function () {
    var n = t.clientWidth || 360;
    t.style.fontSize = n / 360 * 100 + "px";
    };

    if (e.addEventListener) {
    n.addEventListener(o, a, !1);
    e.addEventListener("DOMContentLoaded", a, !1);
    }
    }(window);

补充:

虽然没有看过上面的系列课程,但是好歹做过一些移动端适配的小页面,基本上个人所知道的移动端适配的方法,上面都提到了。

题目 03

2017年腾讯前端实习面试题(二面):用过 CSS3 吗?实现圆角矩形和阴影怎么做?


回答:

当然用过了……

  • 圆角矩形比较简单,可以用 border-radius 这个属性,它可以同时设置四个圆角边框,半径单位可以是像素,也可以是百分比;每个角也可以设置两个不同的半径;设置顺序是:左上、右上、右下、左下,如果只写一个,那么表示设置统一值;也可以分别设置,但比较麻烦。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    .test {
    width: 100px;
    height: 40px;
    border-radius: 4px 6px 5px 3% / 2%;
    /* 相当于 */
    border-top-left-radius: 4px 2%;
    border-top-right-radius: 6px 2%;
    border-bottom-right-radius: 5px 2%;
    border-bottom-left-radius: 3% 2%;
    }
  • 阴影问的应该是盒子阴影吧……

    如果是文字阴影,就一个属性 text-shadow,属性值依次为阴影的:水平位置、垂直位置、模糊距离(程度)、颜色;两个位置是必需的,其他不写范围就是 0 ,颜色默认是文字颜色;可以给同一文字设置多个阴影,多个设置用逗号隔开。

    如果是盒子阴影就又复杂一些,使用 box-shadow 属性,属性值依次为阴影的:水品位置、垂直位置、模糊距离(程度)、大小、颜色、内侧或外侧;位置都是必须的,但是可以为 0 或负值,其他的都好理解,最后一个内侧和外侧意思是将阴影添加到盒子边内部还是外部;多个阴影设置可以用逗号分隔,另外,阴影可以在某些时候模拟边框哟!示例代码:

    1
    2
    3
    .test {
    box-shadow: 3px 3px 5px 5px #ccc, 5px 5px 10px 10px #666; /* 立体效果 */
    }

补充:

基本上用过的话就没什么,主要是把属性顺序搞清楚,不清楚也很容易查到文档;另外分开写的属性,实际很少用。

题目 04

出处同上:什么是闭包,闭包的用途是什么?


回答:

  • 如果一个函数使用了它范围外的变量,那么(这个函数加这个变量)就叫做闭包;也就是说,闭包就是能够读取其他函数内部变量的函数,以及它读取的这个变量的合称!?

  • 用途太广泛了,其实我们经常不可避免的使用着闭包,主要就是用来读取函数范围外的变量,例如:

    1
    2
    3
    4
    5
    6
    7
    var a = 100;

    function sum() {
    return a + 20;
    }

    console.log(sum()); // 120

    另外就是作为钩子,使一些局部变量的值始终保持在内存中,以供外部使用并且不暴露,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    function foo() {
    var num = 100;

    sum = function () {
    num += 10;
    };

    function bar() {
    console.log(num);
    }

    return bar;
    }

    var result = foo();

    result(); // 100
    sum();
    result(); // 110

    循环中也可以使用闭包来解决很多问题,例如常见的循环添加点击事件:

    1
    2
    3
    4
    5
    6
    7
    for (var j = 0; j < btns.length; j++) {
    !function (num) {
    btns[j].onclick = function () {
    console.log('按钮' + (num + 1));
    }
    }(j);
    }

    闭包还可以创建私有变量,外部可以获取和修改,但是却无法得知内部的结构,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    function Person(name) {
    var _age;

    function setAge(age) {
    _age = age;
    }

    function getAge() {
    return _age;
    }

    return {
    name: name,
    setAge: setAge,
    getAge: getAge
    }
    }

    var p1 = new Person('lily');

    p1.setAge(18);
    console.log(p1.getAge()); // 18

补充:

虽然经常在不经意间使用最常见的“闭包”,也能理解闭包的原理,但是实际开发的时候自己用到的复杂一些的闭包不是很多,希望老师讲一些实战中哪些地方常用闭包来解决问题。

题目 05

出处同上:call()apply()bind() 的用法分别是什么?


回答:

这三个方法都能改变调用函数中 this 的指向(作用域),并且可以向调用函数传递参数;三者的用法分别是:

  • call() 方法的第一个参数就是用来设置 this 指向的,第二个及以后的参数是指定的参数列表,适合参数较少时使用;

  • apply() 方法的作用与 call() 方法基本相同,只不过它只接收两个参数,第一个也是 this 的值,而第二个是一个数组,这个数组中可以保存指定的参数列表,即使只传一个参数,也要以数组的形式,故适合参数较多时使用;

  • bind() 方法的参数形式与 call() 方法相同,但使用方式与前两种不同,前两种方法是没有返回值的,而此方法返回一个绑定了固定 this 值的新函数;个人理解就是,前两种是临时工,bind() 方法是合同工……下面是三种方法的示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    const cat = {
    name: '大猫',
    eatFish(...args) {
    console.log(args);
    console.log(this.name + '吃鱼');
    }
    };

    const dog = {
    name: '二哈',
    eatBone(...args) {
    console.log(args);
    console.log(this.name + '吃骨头');
    }
    };

    cat.eatFish.call(dog, '汪汪', 'call'); // 二哈临时可以吃鱼;
    dog.eatBone.call(cat, '喵喵', 'call'); // 大猫临时可以吃骨头;

    cat.eatFish.apply(dog, ['汪汪', 'apply']); // 二哈临时可以吃鱼;
    dog.eatBone.apply(cat, ['喵喵', 'apply']); // 大猫临时可以吃骨头;

    let test1 = cat.eatFish.bind(dog, '汪汪', 'bind');
    let test2 = dog.eatBone.bind(cat, '喵喵', 'bind');

    test1(); // 二哈学会了吃鱼;
    test2(); // 大猫学会了吃骨头;

补充:

call()apply() 常常在继承属性的时候使用,但是 bind() 方法是新出的,个人没怎么接触过,但是都是相关联的方法,用法还是比较好理解的。

题目 06

出处同上:请说出至少 8 个 HTTP 状态码,并描述各状态码的意义。 例如:状态码 200 表示响应成功。


回答:

状态码有 5 类 60 余种,常用的一般 14 种:

  • 1XX:称为“信息性状态码”,表示接收的请求正在处理;不常用。

  • 2XX:称为“成功状态码”,表示请求正常处理完毕。

    • 200 OK,表示从客户端发来的请求在服务器端被正常处理了。

    • 204 No Content,表示服务器接收的请求已成功处理,但在返回的响应报文中不含实体的主体部分。

    • 206 Partial Content,表示客户端进行了范围请求,而服务器成功执行了这部分的 GET 请求。

  • 3XX:称为“重定向状态码”,表示需要进行附加操作以完成请求。

    • 301 Moved Permanently,永久性重定向;表示请求的资源已被分配了新的 URI,以后应使用资源现在所指的 URI。

    • 302 Found,临时性重定向;表示请求的资源已被分配了新的 URI,希望用户(本次)能使用新的 URI 访问。

    • 303 See Other,表示由于请求对应的资源存在着另一个 URI,应使用 GET 方法定向获取请求的资源。

    • 304 Not Modified,表示客户端发送附带条件的请求时,服务器端允许请求访问资源,但因为发生了请求不能满足条件的情况后,便会直接返回此状态码。

    • 307 Temporary Redirect,临时重定向;该状态码与 302 Found 含义相同,但此状态码遵循浏览器标准,不会从 POST 变成 GET。

  • 4XX:称为“客户端错误状态码”,表示服务器无法处理请求。

    • 400 Bad Request,表示请求报文中存在语法错误;但是浏览器会像 200 OK 一样对待该状态码。

    • 401 Unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息。

    • 403 Forbidden,表示对请求资源的访问被服务器拒绝了,服务器没有必要给出拒绝理由,但一般是应为访问权限问题。

    • 404 Not Found,表示服务器上无法找到请求的资源,除此之外,也可以在服务端拒绝请求且不想说明理由时使用。

  • 5XX:称为“服务器错误状态码”,表示服务器处理请求出错。

    • 500 Internal Server Error,表示服务器端在执行请求时发生了错误,也有可能是 Web 应用存在 bug 或某些临时的故障。

    • 503 Service Unavailable,表示服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。

其实状态码经常和状况不一致,但是可能用户察觉不到,这种是正常情况。

状态码查询

补充:

对于状态码,死记硬背也可以,但是会很容易忘记;如果结合实际状况记忆,就会比较深刻了。明明很方便就能查到,而且经验多了自然就记住一些常用的状态码了,不是很理解为什么要考这些。

题目 07

出处同上:请写出一个 HTTP POST 请求的内容,包括四部分。 其中第四部分的内容是username=ff&password=123,第二部分必须含有 Content-Type 字段,请求的路径为 /path。


回答:

1
2
3
4
5
6
POST /path HTTP/1.1
Host: localhost
Content-Type: application/x-www-form-urlencoded
Content-Length: 24

username=ff&password=123

补充:

  • 请求最多包含四部分,最少包含三部分(第四部分可以为空)。

  • 第三部分永远都是一个回车 \n,主要是用来区分第二部分和第四部分内容的。

  • 动词有 GET(获取)、POST(新增或上传)、PUT(整体更新)、PATCH(局部更新)、DELETE(删除)、HEAD、OPTIONS等。

  • 请求中的路径包括“查询参数”,但不包括“锚点”;因为“锚点”不是由服务器识别,而是由浏览器识别的。

  • 如果没有写路径,那么路径默认为 /,代表根目录。

  • 第二部分中的“Content-Type”标注了第四部分的格式。

题目 08

请说出至少三种排序的思路,这三种排序的时间复杂度分别为:

  1. O(n * n)

  2. O(n log2 n)

  3. O(n + max)


回答:

  1. 冒泡排序:从第一个数字开始,每次比较相邻的两个数字,将符合条件的数字(较大或较小)与另一个数字交换位置,如此执行一轮,第一个数字便是符合条件的数字;第二轮则从第二个数字开始,轮次与数字位置关联;最后一轮结束,数字便按顺序排列好了。

  2. 快速排序:在数组中随机选一个数字(或默认第一个数字),数组中小于等于此数字的放在此数字前面,大于此数字的放在此数字后面,完成后,再对这个数字两边的数组进行相同动作,重复此过程直到剩下一个数字便结束。

  3. 基数排序:只能排正整数,基于桶排序;0 ~ 9 共 10 个桶,先根据所有数字的个位数进行入桶,即个位为 0 的按顺序放入 0 组,为 1 的放入 1 组,以此类推;完成个位入桶,在按照 10 个桶的从大到小顺序出桶,出桶完毕后再按所有数字的十位数进行入桶,重复此操作,直到所有数字的最大位数入桶出桶完毕,数字便按顺序排好了。

补充:

常见的第一种还有选择排序,第二种还有堆排序,第三种貌似就有基数排序。

题目 09

著名面试题:一个页面从输入 URL 到页面加载显示完成,这个过程中都发生了什么?

这一题是在挖掘你的知识边界,所以你知道多少就要答多少。


回答:

  1. 浏览器需要知道这个 URL 中包含的域名对应的服务器 IP,那么首先要对域名进行 DNS 解析:浏览器对自己的 DNS 缓存、操作系统的 DNS 缓存以及操作系统的 hosts 文件逐个进行搜索,看看有没有这域名对应的 IP 地址,有便停止搜索,没有便通知操作系统发送一个 DNS 请求到本地的 DNS 服务器,DNS 服务器将解析的 IP 地址返回给操作系统,操作系统缓存一份,然后给浏览器,浏览器也缓存一份,现在浏览器得到了域名对应的 IP 地址。

  2. 得到 IP 后,浏览器便通知主机(你的电脑)开始与服务器通过 TCP 协议建立连接,这个过程分三步,又称为“三次握手”:

    • 主机向服务器发送一个请求,表示建立连接的意愿;

    • 服务器收到请求后,向主机发一个响应信息,表示同意连接;

    • 主机收到了响应信息后,给服务器也发送一个响应信息,然后它们之间的连接建立完成。

  3. 主机与服务器连接成功后,浏览器便开始请求网页数据:

    • 浏览器根据 URL 的内容生成 HTTP 请求,请求中包含文件位置、文件类型等信息,并将请求通过主机发送给服务器;

    • 服务器收到请求,根据 HTTP 请求内容中的要求准备相应的 HTML 文件;

    • 服务器将准备好的 HTML 文件通过主机发送给浏览器;

    • 浏览器边接收边根据 DOM 树解析页面,如果页面上有其他需要的外部文件(如 CSS、JS文件),浏览器会继续向服务器发送请求并接收文件;

  4. 浏览器自上而下加载并展示页面,遇到外部文件请求得到后,加载并解析这些必要的文件,然后才会继续向下加载、渲染页面,这是一个同步(一条道走到黑)的过程,直到页面上所有文件加载、解析、渲染完毕。

  5. 页面展示完成后,主机与服务器断开连接,需要四步,称为“四次挥手”:

    • 主机得知浏览器页面加载完成后,向服务器发送断开连接的请求;

    • 服务器收到断开连接请求后,向主机发送“收到断开请求”的信息,但不会现在断开,因为可能还有数据未发送完毕;

    • 服务器向主机发送“连接可以断开”的信息;

    • 主机收到“连接可以断开”的信息后,向服务器发送“确认连接断开”信息,服务器收到确认信息后便断开连接;

  6. 至此,主机与服务器通信完成,页面也展示完毕了。

补充:

这是在个人理解范围内的粗糙回答,一些技术上的细节因为没有深度了解过,所以也不好照抄,例如 DNS 解析过程中涉及的 LDNS、 TCP 协议连接的过程、页面渲染中涉及的浏览器背后的工作原理等。

题目 10

著名面试题:如何实现数组去重?假设有数组 array = [1, 5, 2, 3, 4, 2, 3, 1, 3, 4],你要写一个函数 unique,使得 unique(array) 的值为 [1, 5, 2, 3, 4],也就是把重复的值都去掉,只保留不重复的值。

要求:不要做多重循环,只能遍历一次请给出两种方案,一种能在 ES 5 环境中运行,一种能在 ES 6 环境中运行(提示 ES6 环境多了一个 Set 对象)。


回答:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var arr = [3, 33, 6, 774, 1235, 23, 3, 33, 321, 6745, 3, 774];

function unique(arr) {
var json = {};
var temp = [];

for (var i = 0; i < arr.length; i++) {
if (!json[arr[i]]) {
temp.push(arr[i]);
json[arr[i]] = 1;
}
}

return temp;
}

function uniqueRe(arr) {
return arr.reduce(function (pre, cur, idx) {
if(arr.indexOf(cur) === idx){
pre.push(cur)
}

return pre;
}, []);
}

console.log(unique(arr)); // [3, 33, 6, 774, 1235, 23, 321, 6745]
console.log(uniqueRe(arr)); // [3, 33, 6, 774, 1235, 23, 321, 6745]

console.log([...new Set(arr)]); // [3, 33, 6, 774, 1235, 23, 321, 6745]

补充:

第一种方法利用对象属性名不能重复的规则,循环数组,以数组项内的数据为属性名,向对象内添加属性,同时将此数据填入新数组;遇到重复数据时,由于有对象属性筛选,所以不会添加到新数组;这种方式只适合简单数组数据,复杂数据还是多重循环靠谱。

第二种是利用 indexOf() 方法只找第一个符合条件的数据下标的特性来完成筛选的。

第三种 ES6 新方法,传入 Set() 对象中的元素只会出现一次,也就是说元素是唯一的。三个点是数组展开语法,也可以将拥有 length 属性的类数组展开为数组;也可以用 Array.form() 方法。

前端系统课程 - 30. jQuery 无缝轮播

无缝轮播

  • 无缝的原理是:图片 1 向左滑动时,图片 2 也同时向左滑动,当图片 1 滑动完成后,立即定位至窗口右侧,准备下一次滑动。

  • 本课涉及的 jQuery API:

    • transitionend CSS 过渡动画完成事件;

    • one() 方法可以只监听一次事件,完成一次后便不再监听;

DOM 事件模型

  • 某些时候需要把函数用字符串表示,例如 HTML 标签内的事件触发调用的函数,定时器内调用的需要参数的函数等;这类型的函数后面是需要加括号的,不然就是普通的字符串了,这种代码一般都会默认调用 eval() 方法解析后运行;而一般情况函数只有在需要运行时,才需要加括号。
  • 使用事件句柄(事件属性)为元素绑定事件监听的缺点在于,不能重复绑定相同事件监听,多次绑定同一种事件监听,后绑定的会覆盖之前绑定的。
  • 如果要绑定多个事件监听,可以使用 addEventListener() 方法;绑定成功的多个事件监听会以队列形式执行,也就是先绑定的先执行;
  • 删除事件监听队列中的项目可以使用 removeEventListener() 方法;要注意,正常情况下绑定和解除绑定是在代码编译过程中便完成了,而不是触发事件之后;除非把解除绑定的代码写在绑定的事件回调函数中。
  • jQuery 中的 one() 方法就是将解除事件绑定的代码写入了绑定事件的回调函数中,这样在当次代码运行后,事件只会触发一次。
  • addEventListener() 方法的第一个函数是事件名称,第二个是事件触发成功的回调函数,第三个参数接收一个布尔值,代表绑定相同事件时,触发事件后回调函数执行的顺序:默认为假,则事件从子元素向上传递,如果传入真值,则从父元素向下传递;但是在触发事件的时候,浏览器会先从父元素向下将事件触发捕获,无论这些元素是否绑定了事件监听或有无回调函数,再通过传入的参数决定按什么顺序执行触发后的回调函数;如果捕获和冒泡都在同一级,便按添加顺序执行。
  • 转到下一节……

前端系统课程 - 29. jQuery 做轮播图

代码分离

  • 内容、样式、行为分离,在当下是一种大多数开发者认为理所当然的共识。

  • 代码分离解决的最主要的问题是代码维护。

  • 理所当然的问题,可以用“如果不”的方式去回答,例如设想使用 HTML 文件操作样式会使更新样式工作繁复;使用 CSS 操作内容导致内容无法被选中、获取或搜索;使用 JS 操作样式有可能改变样式布局(举例了 jQuery 中的 show()hide() 方法)等等。

轮播

  • 利用”拉洋片“的原理……

  • 朝哪个方向滑动,就把图片朝哪个方向连起来。

  • 尽量使用 CSS3 的代码,现在的兼容性已经很好了。

  • 在知道图片宽高时,尽量将 <img> 标签的 widthheight 属性填写上,以便在图片无法显示时,也不会影响布局。

  • 取模运算可以用来配合循环定时器,n % length 可以将操作轮次一直控制在固定的长度范围内。例如:

    1
    2
    3
    4
    5
    var n = 0;
    setInterval(() => {
    console.log(n % 3);
    n++;
    }, 1000); // 每秒会打印出n对3取的余数,也就是0、1、2如此循环
  • 本课涉及的 jQuery API:

    • on() 方法,第一个参数是监听的事件,第二个是要执行的函数,这个函数也可以接收事件信息参数;

    • css() 方法,传入一个对象,对象中是要修改的 CSS 样式代码;

    • index() 方法可以获取元素在兄弟节点中的排序;

    • trigger() 方法可以激活参数指定的事件;

    • eq() 方法可以获取 jQuery 对象列表中指定下标的某个元素,这个元素任然是 jQuery 对象而不是 DOM 对象,这就是此方法区别于直接使用方括号或 get() 方法的特性;

    • addClass() 方法可以给对象添加指定的 class 属性,与之相对应的删除方法是 removeClass()

    • siblings() 方法可以获取对象的所有兄弟元素节点;

    • 要创建一个标签,可以直接在 $() 方法中以字符串形式传入标签,例如:$('<li></li>') 就可以创建一个 <li> 标签;

    • appen() 方法可以将新建的 jQuery 对象添加为目标元素的子元素;

    • 如果要对多个元素进行相同操作,可以将多个元素一并选中,并用逗号隔开,例如:$('.test1, .test2')

前端系统课程 - 28. jQuery 不过如此

封装函数

尝试将一个功能,封装为一个函数,利用函数的参数,将数据抽象化后,函数便拥有了更高的适用性。例如来封装一个获取某个节点的所有兄弟节点的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
function getSibilings(node) {
var childs = node.parent.children,
sibilings = {
length: 0
}
for (var i = 0; i < childs.length; i++){
if(childs[i] !== node){
sibilings[length] = childs[i];
sibilings.lengeh++;
}
}
return sibilings;
}

上面的示例中,把要找兄弟节点的那个节点,当作参数传入函数内,然后获取到它的父节点,再获取父节点的所有子元素集合,循环这个集合,排除目标节点后,将其余的保存为一个对象并返回。

命名空间

为了防止全局变量污染造成的麻烦,就要尽量少用全局变量,那么用什么办法能让要全局使用的属性不是全局变量呢?可以使用一个全局变量,把需要统一的全局属性,挂载到这个全局变量中即可,这就是命名空间。

简易 jQuery

  • 在某个对象的原型上增加一个方法,那么这个对象的实例都会拥有这个方法。

  • 修改原型一般是不推荐的,因为你不知道是否有别人是否会修改同名属性;这时可以新添加一个全局属性来补充原型。例如为 Node 对象的某个实例增加方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    window.NodeUp = function (nodeOrSelector) {
    var nodes = {length: 0};
    if (typeof nodeOrSelector === 'string') {
    nodeOrSelector = document.querySelectorAll(nodeOrSelector);
    for (var i = 0; i < nodeOrSelector.length; i++) {
    nodes[i] = nodeOrSelector[i];
    nodes.length++;
    }
    } else if(nodeOrSelector instanceof Node) {
    nodes[0] = nodeOrSelector;
    nodes.length++;
    }

    nodes.addClass = function (classes) {
    var isArr = Array.isArray(classes);
    for (var i = 0; i < nodes.length; i++) {
    if (!isArr) {
    nodes[i].classList.add(classes);
    } else {
    for (var j = 0; j < classes.length; j++) {
    nodes[i].classList.add(classes[j]);
    }
    }
    }
    };

    nodes.setText = function (text) {
    if (text) {
    if (!(typeof text === 'string')) {
    return;
    }
    for (var i = 0; i < nodes.length; i++) {
    nodes[i].textContent = text;
    }
    } else {
    var str;
    for (var j = 0; j < nodes.length; j++) {
    str += nodes[j].textContent;
    }
    return str;
    }
    }

    return nodes;
    };

    window.$ = NodeUp;
  • 上面的简单例子中,如果把 NodeUp 改成 jQuery,可以吗?当然……参数接收一个字符串选择器或者 DOM 对象,就会返回一个新的对象,这个新对象函数中已经被添加了新的属性和方法;当传入某个参数使其实例化后,这个实例对象,就可以使用这些新属性和方法了。

  • 如果简化了代码可能会觉得哪里不对,其实是因为忽略了实例化时传入参数的过程,例如下面的简化例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    window.NodeUp = function (node) {
    return {
    next: function () {
    return node.nextSibling;
    },
    addClass: function (class) {
    node.classList.add(class);
    }
    }
    }

    var nodeUp = NodeUp(testObj);
    nodeUp.addClass('test');

    上面代码中的 NodeUp 返回的是一个拥有两个方法的对象,那么这个对象是包装后的 DOM 对象吗?不是。当 NodeUp 实例化时,不管有没有参数传入,实例化对象都可以使用这两个方法;只不过这两个方法又依赖于传进来的参数,也就是需要一个 DOM 对象,所以实例化后,就看起来好像是传入的这个 DOM 对象拥有了这两个方法,其实并不是,只是通过参数来告诉函数,调用这两个方法的时候,要施加给谁;而前面较为复杂的那段代码,才是真正的把方法添加给了包装后的 DOM 对象。要想证明这点很简单,上个例子中的对象可以剥离出 DOM 对象,而这个例子中的对象是不行的。

  • jQuery 中的 set 和 get 两种类型的方法,大都是根据是否传参来判断执行何种操作的。

课后拾遗

  • jQuery 文件有三种,分别是:

    • 以 .js 结尾:这种是未压缩的文件,又称为“开发版”;这种版本与源码没什么区别,注释、各种参数介绍都很齐全,是在开发的时候方便开发者“追根溯源”的,一旦有需要查看源代码,便可以找到相应的位置和注释。

    • 以 .min.js 结尾:这种是压缩过的文件,又称为“线上版”或“生产版”;这种版本将代码中能缩短的标识符尽量缩短以精简代码,并删除了注释等信息,从而将文件体积压缩到最小,使下载时消耗更少的流量,下载速度更快。

    • 以 .min.map 结尾:这种是索引文件(source map),一般与第一种配合使用,文件中记录了代码转换和压缩前的状态和位置信息,如果压缩后的文件运行时发生了错误,可以依靠这个文件进行排错;这个功能需要浏览器支持。

  • jQuery 对象和 DOM 对象的联系和区别:

    • 联系:说是联系,其实是转换方法。将 DOM 对象传入 jQuery,可以把 DOM 对象变成 jQuery 对象,例如:$(div),此时 jQuery 会把 DOM 对象“包装”(封装)成一个 jQuery 对象;而使用 jQuery 提供的 get() 方法,或直接使用下标,就可以把 jQuery 对象变成 DOM 对象,例如:$div.get(0)$div[0],由于 jQuery 对象是一个伪数组,所以要通过下标 0 来获取第一个元素,即便其中只有一个元素;虽然只有一个元素时 get() 方法不传参也能获取到,但这样获取到的和原对象是不一样的。

    • 区别:其实很简单,就是原型链的不同,所以它们拥有不同的 API,也因此不能混用。

  • 拥有 id 属性的标签,在 JavaScript 中默认成为 window 对象的属性,相当于一个全局变量;也正因为如此,所以 id 的值不要与 window 对象已有的属性重名;这也是尽量不用全局变量的原因之一。