JavaScript

132次阅读

共计 13830 个字符,预计需要花费 35 分钟才能阅读完成。

new 一个对象,这个过程中发生了什么?

var obj = new Object("name","sansan");
  1. 创建一个新对象,如:var obj = {};
  2. 新对象的_proto_属性指向构造函数的原型对象。
  3. 将构造函数的作用域赋值给新对象。(也所以 this 对象指向新对象)
  4. 执行构造函数内部的代码,将属性添加给 obj 中的 this 对象。
  5. 返回新对象 obj。

数组中常用的方法

改变原数组

splice() 添加 / 删除数组元素

let a = [1, 2, 3, 4, 5, 6, 7];
let item = a.splice(0, 3); // [1,2,3]
console.log(a); // [4,5,6,7]
// 从数组下标 0 开始,删除 3 个元素
let item1 = a.splice(0,3,'添加'); // [4,5,6]
console.log(a); // ['添加',7]
// 从数组下标 0 开始,删除 3 个元素,并添加元素 '添加'

sort() 数组排序

var array =  [10, 1, 3, 4,20,4,25,8];
 // 升序 a-b < 0   a 将排到 b 的前面,按照 a 的大小来排序的
 array.sort(function(a,b){return a-b;});
 console.log(array); // [1,3,4,4,8,10,20,25];
 // 降序
 array.sort(function(a,b){return b-a;});
 console.log(array); // [25,20,10,8,4,4,3,1];

pop() 删除一个数组中的最后的一个元素

shift() 删除数组的第一个元素

push() 向数组的末尾添加元素

unshift()向数组开头添加元素

reverse()

let  a =  [1,2,3];
  a.pop();  // 3, 返回被删除的元素
  console.log(a); // [1,2]
  a.shift(); // 1
  console.log(a); // [2]
  a.push("末尾添加");  // 2 , 返回数组长度
  console.log(a) ; [2,"末尾添加"]
  a.unshift("开头添加"); // 3
  console.log(a); //["开头添加", 2, "末尾添加"]
  a.reverse();   // ["末尾添加", 2, "开头添加"]
  console.log(a) // ["末尾添加", 2, "开头添加"]

copyWithin() 指定位置的成员复制到其他位置

let a = ['zhang', 'wang', 'zhou', 'wu', 'zheng'];
 // 1 位置开始被替换, 2 位置开始读取要替换的  5 位置前面停止替换
 a.copyWithin(1, 2, 5);
 // ["zhang", "zhou", "wu", "zheng", "zheng"]

fill() 填充数组

['a', 'b', 'c'].fill(7)
// [7, 7, 7]
['a', 'b', 'c'].fill(7, 1, 2)
// ['a', 7, 'c']

不改变原数组

join() 数组转字符串

let a= ['hello','world'];
 let str2=a.join('+'); // 'hello+world'

cancat 合并两个或多个数组

let a = [1, 2, 3];
  let b = [4, 5, 6];
  // 连接两个数组
  let newVal=a.concat(b); // [1,2,3,4,5,6]

ES6 扩展运算符 … 合并数组

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

indexOf() 查找数组是否存在某个元素,返回下标

let a=['啦啦',2,4,24,NaN]
console.log(a.indexOf('啦'));  // -1
console.log(a.indexOf('啦啦')); // 0

ES7 includes() 查找数组是否包含某个元素 返回布尔

indexOf 方法不能识别 NaN

indexOf 方法检查是否包含某个值不够语义化,需要判断是否不等于 -1,表达不够直观

let a=['OB','Koro1',1,NaN];
 a.includes(NaN); // true 识别 NaN
 a.includes('Koro1',100); // false 超过数组长度 不搜索
 a.includes('Koro1',-3);  // true 从倒数第三个元素开始搜索

slice() 浅拷贝数组的元素

// 字符串也有一个 slice() 方法是用来提取字符串的,不要弄混了。let a = [{name: 'OBKoro1'}, {name: 'zhangsan'}];
 let b = a.slice(0,1);
 console.log(b, a);
 // [{"name":"OBKoro1"}]  [{"name":"OBKoro1"}]
 a[0].name='改变原数组';
 console.log(b,a);
 // [{"name":"改变原数组"}] [{"name":"改变原数组"}]

遍历方法

forEach: 按升序为数组中含有效值的每一项执行一次回调函数

1. 无法中途退出循环,只能用 return 退出本次回调,进行下一次回调.
2. 它总是返回 undefined 值, 即使你 return 了一个值。

every 检测数组所有元素是否都符合判断条件

如果数组中检测到有一个元素不满足, 则整个表达式返回 false, 且元素不会再进行检测

function isBigEnough(element, index, array) {return element >= 10; // 判断数组中的所有元素是否都大于 10}
[12, 5, 8, 130, 44].every(isBigEnough);   // false
[12, 54, 18, 130, 44].every(isBigEnough); // true
// 接受箭头函数写法
[12, 5, 8, 130, 44].every(x => x >= 10); // false
[12, 54, 18, 130, 44].every(x => x >= 10); // true

some 数组中的是否有满足判断条件的元素

如果有一个元素满足条件,则表达式返回 true, 剩余的元素不会再执行检测

filter 过滤原始数组,返回新数组

map 对数组中的每个元素进行处理,返回新的数组

reduce 为数组提供累加器,合并为一个值

reduce() 方法对累加器和数组中的每个元素(从左到右)应用一个函数,最终合并为一个值

// 数组求和
let sum = [0, 1, 2, 3].reduce(function (a, b) {return a + b;}, 0);
// 6
// 将二维数组转化为一维 将数组元素展开
let flattened = [[0, 1], [2, 3], [4, 5]].reduce((a, b) => a.concat(b),
  []);
// [0, 1, 2, 3, 4, 5]

ES6:find()& findIndex() 根据条件找到数组成员

这两个方法都可以识别 NaN, 弥补了 indexOf 的不足.

[1, 4, -5, 10,NaN].find((n) => Object.is(NaN, n));
// 返回元素 NaN
[1, 4, -5, 10].findIndex((n) => n < 0);
// 返回索引 2 

ES6 keys()&values()&entries() 遍历键名、遍历键值、遍历键名 + 键值

for (let index of ['a', 'b'].keys()) {console.log(index);
}
// 0
// 1
for (let elem of ['a', 'b'].values()) {console.log(elem);
}
// 'a'
// 'b'
for (let [index, elem] of ['a', 'b'].entries()) {console.log(index, elem);
}
// 0 "a"
// 1 "b"

判断对象是否为空

const isEmpty = (obj) => obj.keys().length !== 0;

判断对象是否是数组

方法一:使用 Object.prototype.toString 来判断是否是数组

function isArray(obj){return Object.prototype.toString.call( obj) === '[object Array]';
}

这里使用 call 来使 toString 中 this 指向 obj。进而完成判断

方法二:使用原型链 来完成判断

function isArray(obj){return obj.__proto__ === Array.prototype;}

基本思想: 实例如果是某个构造函数构造出来的那么 它的__proto__是指向构造函数的 prototype 属性

继承有哪些方式?

  • ES6 中的 class 继承
class Animal {constructor(name) {this.name = name;};
  eat() {console.log(this.name + '正在吃东西');
  };
}
// 继承动物类
class Cat extends Animal {catchMouse(){console.log(`${this.name}正在捉老鼠 `);
  }
}
var cat= new Cat('Tom 猫');
cat.catchMouse();// Tom 猫正在捉老鼠
  • 原型继承
function Animal(name) {this.name = name;}

Animal.prototype.eat= function () {console.log(this.name + '正在吃东西')
};
function Cat(furColor){this.furColor = furColor ;};
Cat.prototype = new Animal();
let tom = new Cat('black');
console.log(tom)
  • 构造继承
  • 寄生组合式继承
  • 实例继承

call、apply、bind 之间的关系

bind,apply,call 三者都可以用来改变 this 的指向;

  • 三者都可以用来改变 this 的指向
  • 三者第一个参数都是 this 要指向的对象,也就是想指定的上下文,上下文就是指调用函数的那个对象。(点前的那个对象,没有就是全局 window)
  • 三者都可以传参,但是 apply 是数组,而 call 是有顺序的传入
  • bind 是返回对应函数,便于稍后调用;apply、call 则是立即执行

异步过程的构成要素有哪些?和异步过程是怎样的?

总结一下,一个异步过程通常是这样的:

  • 主线程发起一个异步请求,相应的工作线程接收请求并告知主线程已收到(异步函数返回);
  • 主线程可以继续执行后面的代码,同时工作线程执行异步任务;
  • 工作线程完成工作后,通知主线程;
  • 主线程收到通知后,执行一定的动作(调用回调函数)。
  1. 异步函数通常具有以下的形式:A(args…, callbackFn)。
  2. 它可以叫做异步过程的发起函数,或者叫做异步任务注册函数。
  3. args 和 callbackFn 是这个函数的参数。

所以,从主线程的角度看,一个异步过程包括下面两个要素:

  • 发起函数(或叫注册函数) A。
  • 回调函数 callbackFn。

它们都是在主线程上调用的,其中注册函数用来发起异步过程,回调函数用来处理结果。

举个具体的例子:

setTimeout(fn, 1000);

其中的 setTimeout 就是异步过程的发起函数,fn 是回调函数。

注意:前面说的形式 A(args…, callbackFn) 只是一种抽象的表示,并不代表回调函数一定要作为发起函数的参数。

例如:

var xhr = new XMLHttpRequest();
xhr.onreadystatechange = xxx; // 添加回调函数
xhr.open('GET', url);
xhr.send(); // 发起函数

发起函数和回调函数就是分离的。

说说消息队列和事件循环

  • 主线程在执行完当前循环中的所有代码后,就会到消息队列取出这条消息(也就是 message 函数),并执行它。
  • 完成了工作线程对主线程的通知,回调函数也就得到了执行。
  • 如果一开始主线程就没有提供回调函数,AJAX 线程在收到 HTTP 响应后,也就没必要通知主线程,从而也没必要往消息队列放消息。

异步过程的回调函数,一定不在当前的这一轮事件循环中执行

eval() 函数有什么用?

eval() 函数可计算某个字符串,并执行其中的的 JavaScript 代码。

字符串转数组

console.log(Object.values('abc')); // => ['a', 'b', 'c']
console.log(Array.from('abcd'));
console.log([...'abcd']);
console.log('abcd'.split(''));
console.log([].slice.call('abcd'));

构造元素重复的数组

可以使用 array.fill:

const arr = Array(6).fill(6);
console.log(arr); // => [6, 6, 6, 6, 6, 6]

构造元素连续的数组

// length 设置为你需要获取的整数的个数,index + 1 这个 1 可以替换为你要设置的第一个数的大小
const continuousIntegers = Array.from({length: 10}, (__, index) => index + 1);
console.log(continuousIntegers); // => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

JS 中如何将页面重定向到另一个页面?

  1. 使用 window.location.href =“https://www.onlineinterviewquestions.com/”
  2. 使用 location.replace:window.location.replace("https://www.onlineinterviewquestions.com/;")

JS 中的 Array.splice()Array.slice()方法有什么区别

var arr=[0,1,2,3,4,5,6,7,8,9];// 设置一个数组
console.log(arr.slice(2,7));//2,3,4,5,6
console.log(arr.splice(2,7));//2,3,4,5,6,7,8
// 由此我们简单推测数量两个函数参数的意义,
slice(start,end)第一个参数表示开始位置, 第二个表示截取到的位置(不包含该位置)
splice(start,length)第一个参数开始位置, 第二个参数截取长度
var x=y=[0,1,2,3,4,5,6,7,8,9]
console.log(x.slice(2,5));//2,3,4
console.log(x);[0,1,2,3,4,5,6,7,8,9]原数组并未改变
// 接下来用同样方式测试 splice
console.log(y.splice(2,5));//2,3,4,5,6
console.log(y);//[0,1,7,8,9]显示原数组中的数值被剔除掉了

如何在 JS 中动态添加 / 删除对象的属性?

咱们可以使用 object.property_name = value 向对象添加属性,delete object.property_name用于删除属性。

例如:

let user = new Object();
// adding a property
user.name='Anil';
user.age  =25;
console.log(user);
delete user.age;
console.log(user);

解释一下什么是 promise?

promise是 js 中的一个对象,用于生成可能在将来产生结果的值。值可以是已解析的值,也可以是说明为什么未解析该值的原因。

promise 可以有三种状态:

  • pending:初始状态,既不是成功也不是失败
  • fulfilled:意味着操作完全成功
  • rejected:意味着操作失败

一个等待状态的 promise 对象能够成功后返回一个值,也能失败后带回一个错误 当这两种情况发生的时候,处理函数会排队执行通过 then 方法会被调用

数组去重复的方法有哪些

1. 使用setfunction uniquearray(array) {let unique_array= Array.from(set(array)) return unique_array; }

2. 使用filter

function unque_array (arr) {let unique_array = arr.filter(function(elem, index, self) {return index == self.indexOf(elem);
  })
  return unique_array;
}

 console.log(unique_array(array_with_duplicates));

3. 使用 for 循环

Array dups_names = ['Ron', 'Pal', 'Fred', 'Rongo', 'Ron'];
function dups_array(dups_names) {let unique = {};
 names.forEach(function(i) {If (!unique[i]) {unique[i] = true;    }
  });
return Object.keys(unique);}   // Ron, Pal, Fred, Rongo
Dups_array(names);

undefined,null 和 undeclared 有什么区别?

1.null 表示 ” 没有对象 ”,即该处不应该有值,转为数值时为 0。典型用法是:

(1)作为函数的参数,表示该函数的参数不是对象。

(2)作为对象原型链的终点。

2.undefined 表示 ” 缺少值 ”,就是此处应该有一个值,但是还没有定义,转为数值时为 NaN。典型用法是:

(1)变量被声明了,但没有赋值时,就等于 undefined。

(2) 调用函数时,应该提供的参数没有提供,该参数等于 undefined。

(3)对象没有赋值的属性,该属性的值为 undefined。

(4)函数没有返回值时,默认返回 undefined。

3.undeclared:js 语法错误,没有申明直接使用,js 无法找到对应的上下文。

JS 中 == 和 === 区别是什么?

1、对于 string,number 等基础类型,===== 有区别
1)不同类型间比较,== 之比较“转化成同一类型后的值”看“值”是否相等,===如果类型不同,其结果就是不等。2)同类型比较,直接进行“值”比较,两者结果一样。
2、对于 Array,Object 等高级类型,===== 没有区别

进行“指针地址”比较。

3、基础类型与高级类型,===== 有区别
1)对于==,将高级转化为基础类型,进行“值”比较。
2)因为类型不同,=== 结果为false

解释 JS 中的事件冒泡和事件捕获

事件捕获和冒泡: 在 HTML DOM API 中,有两种事件传播方法,它们决定了接收事件的顺序。两种方法是事件冒泡和事件捕获。第一个方法事件冒泡将事件指向其预期的目标,第二个方法称为事件捕获,其中事件向下到达元素。

事件捕获

捕获过程很少被使用,但是当它被使用时,它被证明是非常有用的。这个过程也称为 滴流模式。在这个过程中,事件首先由最外层的元素捕获,然后传播到最内部的元素。例如:

<div>
  <ul>
    <li></li>
  </ul>
</div>

从上面的示例中,假设单击事件发生在 li 元素中,在这种情况下,捕获事件将首先处理div,然后处理ul,最后命中目标元素li

事件冒泡

冒泡的工作原理与冒泡类似,事件由最内部的元素处理,然后传播到外部元素。

<div>
  <ul>
    <li></li>
  </ul>
</div>

从上面的例子中,假设 click 事件确实发生在冒泡模型中的 li 元素中,该事件将首先由 li 处理,然后由 ul 处理,最后由 div 元素处理。

module.exports 和 exports 之间有什么区别?

moduleexportsNode.js给每个 js 文件内置的两个对象。可以通过 console.log(module)console.log(exports)打印出来。如果你在 main.js 中写入下面两行,然后运行$ node main.js:

console.log(exports);// 输出:{}
console.log(module);// 输出:Module {..., exports: {}, ...}(注:... 代表省略了其他一些属性)

从打印咱们可以看出,module.exportsexports 一开始都是一个空对象 {},实际上,这两个对象指向同一块内存。这也就是说module.exportsexports是等价的(有个前提:不去改变它们指向的内存地址)。

例如:exports.age = 18module.export.age = 18,这两种写法是一致的(都相当于给最初的空对象{} 添加了一个属性,通过 require 得到的就是{age: 18}

import 和 exports 是什么?

importexports 帮助咱们编写模块化的 JS 代码。使用 importexports,咱们可以将代码分割成多个文件。import只允许获取文件的某些特定变量或方法。可以导入模块导出的方法或变量。

//index.js

 import name,age from './person';

 console.log(name);
 console.log(age);

 //person.js

 let name ='Sharad', occupation='developer', age =26;

 export {name, age};

如何在 JS 中克隆对象

Object.assign()方法用于在 JS 中克隆对象。如:

var x = {myProp: "value"};
var y = Object.assign({}, x);

如何在 JS 中编码和解码 URL?

var uri = "my profile.php?name=sammer&occupation=pāntiNG";
var encoded_uri = encodeURI(uri);
decodeURI(encoded_uri);

BOM 和 DOM 的关系

BOM全称Browser Object Model,即浏览器对象模型,主要处理浏览器窗口和框架。

DOM全称Document Object Model,即文档对象模型,是 HTML 和 XML 的应用程序接口(API),遵循 W3C 的标准,所有浏览器公共遵守的标准。

JS 是通过访问 BOM(Browser Object Model)对象来访问、控制、修改客户端(浏览器),由于BOMwindow包含了 documentwindow 对象的属性和方法是直接可以使用而且被感知的,因此可以直接使用 window 对象的 document 属性,通过 document 属性就可以访问、检索、修改 XHTML 文档内容与结构。因为 document 对象又是 DOM 的根节点。

可以说,BOM包含了 DOM(对象),浏览器提供出来给予访问的是BOM 对象,从 BOM 对象再访问到 DOM 对象,从而 js 可以操作浏览器以及浏览器读取到的文档。

列出一些内置方法及其返回的值

内置方法返回值 CharAt()它返回指定索引处的字符。Concat()它连接两个或多个字符串。forEach()它为数组中的每个元素调用一个函数。indexOf()它返回指定值第一次出现时调用字符串对象中的索引。length()它返回字符串的长度。pop()它从数组中删除最后一个元素并返回该元素。push()它将一个或多个元素添加到数组的末尾,并返回数组的新长度。reverse()反转数组元素的顺序。

null 和 undefined 区别

undefined是基本数据类型 表示未定义 缺少的意思。

null是引用数据类型,是对象,表示空对象

undefined是从 null 派生出来的 所以 undefined==nulltrue

undeclared 和 undefined 区别?

undeclared 的变量是程序中不存在且未声明的变量。如果程序尝试读取未声明变量的值,则会遇到运行时错误。undefined 的变量是在程序中声明但未赋予任何值的变量,如果程序试图读取未定义变量的值,则返回 undefined 的值。

call 和 apply 有什么区别

callapply 可以用来重新定义函数的执行环境,也就是 this 的指向;callapply 都是为了改变某个函数运行时的 context,即上下文而存在的,换句话说,就是为了改变函数体内部this 的指向。

call()调用一个对象的方法,用另一个对象替换当前对象,可以继承另外一个对象的属性,它的语法是:

Function.call(obj[, param1[, param2[, [,...paramN]]]]);

说明:call方法可以用来代替另一个对象调用一个方法,call方法可以将一个函数的对象上下文从初始的上下文改变为 obj 指定的新对象,如果没有提供 obj 参数,那么 Global 对象被用于obj

apply()call() 方法一样,只是参数列表不同,语法:

Function.apply(obj[, argArray]);

说明 :如果argArray 不是一个有效数组或不是 arguments 对象,那么将导致一个 TypeError,如果没有提供argArrayobj任何一个参数,那么 Global 对象将用作 obj。

如何在 JS 中清空数组

有许多方法可以用来清空数组:

方法一:

arrayList = []

上面的代码将把变量 arrayList 设置为一个新的空数组。如果在其他任何地方都没有对原始数组 arrayList 的引用,则建议这样做,因为它实际上会创建一个新的空数组。咱们应该小心使用这种清空数组的方法,因为如果你从另一个变量引用了这个数组,那么原始的引用数组将保持不变。

方法二:

arrayList.length = 0;

上面的代码将通过将其 length 设置为 0 来清除现有数组。这种清空数组的方式还会更新指向原始数组的所有引用变量。因此,当你想要更新指向 arrayList 的所有引用变量时,此方法很有用。

方法三:

arrayList.splice(0, arrayList.length);

这处方法也行,当然这种清空数组的方法也将更新对原始数组的所有引用。

方法四:

while(arrayList.length)
{arrayList.pop();
}

上面的实现也可以空数组,但通常不建议经常使用这种方式。

new 操作符具体干了什么呢 ?

  • 创建一个空对象,并且 this 变量引用该对象,同时还继承了该函数的原型。
  • 属性和方法被加入到 this 引用的对象中。
  • 新创建的对象由 this 所引用,并且最后隐式的返回 this。

把伪数组转换为数组

只需使用 [].slice.call(elements) 即可实现:

var elements = document.querySelectorAll("p"); // NodeList
var arrayElements = [].slice.call(elements); // 现在 NodeList 是一个数组

var arrayElements = Array.from(elements); // 这是另一种转换 NodeList 到 Array  的方法

JavaScript 判断一个变量是对象还是数组?

typeof 都返回 object

在 JavaScript 中所有数据类型严格意义上都是对象,但实际使用中我们还是有类型之分,如果要判断一个变量是数组还是对象使用 typeof 搞不定,因为它全都返回 object。

第一,使用 typeof 加 length 属性

数组有 length 属性,object 没有,而 typeof 数组与对象都返回 object,所以我们可以这么判断

var getDataType = function(o){if(typeof o == 'object'){if( typeof o.length == 'number'){return 'Array';} else {return 'Object';}
    } else {return 'param is no object type';}
};

第二,使用 instanceof

利用 instanceof 判断数据类型是对象还是数组时应该优先判断 array,最后判断 object。

var getDataType = function(o){if(o instanceof Array){return 'Array'} else if (o instanceof Object){return 'Object';} else {return 'param is no object type';}
};

如何将一个数组打乱

方法 1:arr.sort(() => Math.random() - 0.5);

sort 方法使用了插入排序(目标长度小于 10)和快排,元素之间的比较远小于 n(n-1)/2,因此有些元素间没有随机交换的可能,使得该方法不够随机。

方法 2:Fisher-Yates Shuffle,复杂度为 O(n)。从后向前遍历,不断将当前元素与随机位置的元素(除去已遍历的部分)进行交换。

function shuffle(arr) { 
    let m = arr.length; 
    while (m > 1){let index = Math.floor(Math.random() * m--); 
        [arr[m] , arr[index]] = [arr[index] , arr[m]] 
    } 
    return arr; 
} 
var list = [1, 2, 3];
console.log(list.sort(function() {Math.random() - 0.5 })); // [2, 1, 3]

MVVM

MVVM 是 Model-View-ViewModel 的缩写,Model 代表数据模型,View 代表用户操作界面,ViewModel 则是视图数据层。ViewModel 通过双向数据绑定将 View 和 Model 层连接了起来,开发人员不用手动操作 Dom 元素,View 和 Mode 会自动双向同步,开发者只需要关注业务逻辑即可。

图片懒加载

先将 img 标签的 src 链接设为同一张图片(比如某空白图片),然后给 img 标签设置自定义属性(比如 data-src),并将真正的图片地址存储在其中。当 js 监听到该图片元素进入可视窗口时(获取 img 节点距离浏览器顶部的距离,如果小于或等于浏览器窗口的可视高度即进入),再将自定义属性中的地址存储到 src 属性中,达到懒加载的效果。这样不仅可以减轻服务器压力,也可以提高用户体验。

把 Script 放 body 前后有什么区别?浏览器会如何解析它们?

html 标签只包含 head 和 body 两个标签,解析时,所有标签都会解析进这两个标签里边。body 之前的任何位置都会解析进 head 里边,之后的都会解析进 body 里边。

js 延迟加载的方式有哪些?

共有:defer 和 async、动态创建 DOM 方式(用得最多)、使用 jQuery 的 getscript 方法、使用 settimeout 延迟方法、让 js 最后加载
defer 属性:(页面 load 后执行)

HTML 4.01 为 <script>标签定义了 defer属性。
用途:表明脚本在执行时不会影响页面的构造。也就是说,脚本会被延迟到整个页面都解析完毕之后再执行

<script> 元素中设置 defer 属性,等于告诉浏览器立即下载,但 延迟执行

<html>
<head>
    <script src="test1.js" defer="defer"></script>
    <script src="test2.js" defer="defer"></script>
</head>
<body>
<!-- 这里放内容 -->
</body>
</html> 

说明:虽然 <script> 元素放在了 <head> 元素中,但包含的脚本将延迟浏览器遇到 </html> 标签后再执行。
HTML5 规范要求脚本按照它们出现的先后顺序执行。在现实当中,延迟脚本并不一定会按照顺序执行。
defer 属性只适用于外部脚本文件。支持 HTML5 的实现会忽略嵌入脚本设置的 defer 属性。

  1. async 属性

HTML5 为 <script> 标签定义了 async 属性。与 defer 属性类似,都用于改变处理脚本的行为。同样,只适用于外部脚本文件。
目的:不让页面等待脚本下载和执行,从而异步加载页面其他内容。

异步脚本一定会在页面 load 事件前执行。
不能保证脚本会按顺序执行。

<!DOCTYPE html>
<html>
<head>
    <script src="test1.js" async></script>
    <script src="test2.js" async></script>
</head>
<body>
<!-- 这里放内容 -->
</body>
</html>  

async 和 defer 一样,都不会阻塞其他资源下载,所以不会影响页面的加载。
缺点:不能控制加载的顺序
3. 动态创建 DOM 方式

// 这些代码应被放置在 </body> 标签前(接近 HTML 文件底部)
<script type="text/javascript">  
   function downloadJSAtOnload() {varelement = document.createElement("script");  
       element.src = "defer.js";  
       document.body.appendChild(element);  
   }  
   if (window.addEventListener)  
      window.addEventListener("load",downloadJSAtOnload, false);  
   else if (window.attachEvent)  
      window.attachEvent("onload",downloadJSAtOnload);  
   else 
      window.onload =downloadJSAtOnload;  
</script>  

4. 使用 jQuery 的 getScript()方法

$.getScript("outer.js",function(){// 回调函数,成功获取文件后执行的函数  
      console.log("脚本加载完成")  
});

5. 使用 setTimeout 延迟方法
6. 让 JS 最后加载
把 js 外部引入的文件放到页面底部,来让 js 最后引入,从而加快页面加载速度

正文完
 0

JavaScript

136次阅读

共计 3195 个字符,预计需要花费 8 分钟才能阅读完成。

前言
作为前端高频面试题之一,相信很多小伙伴都有遇到过这个问题。那么你是否清楚完整的了解它呢?
国际惯例,让我们先抛出问题:

什么是原型、原型链
它们有什么特点
它们能做什么
怎么确定它们的关系

或许你已经有答案,或许你开始有点疑惑,无论是 get 新技能或是简单的温习一次,让我们一起去探究一番吧
如果文章中有出现纰漏、错误之处,还请看到的小伙伴多多指教,先行谢过
以下↓
原型
JavaScript 是基于原型的我们创建的每个函数都有一个 prototype(原型) 属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。

简单来说,就是当我们创建一个函数的时候,系统就会自动分配一个 prototype 属性,可以用来存储可以让所有实例共享的属性和方法
用一张图来表示就更加清晰了:
图解:

每一个构造函数都拥有一个 prototype 属性,这个属性指向一个对象,也就是原型对象
原型对象默认拥有一个 constructor 属性,指向指向它的那个构造函数
每个对象都拥有一个隐藏的属性 __prototype__,指向它的原型对象

function Person(){}

var person = new Person();

person.__proto__ === Person.prototype // true

Person.prototype.constructor === Person // true
那么,原型对象都有哪些特点呢
原型特点
function Person(){}
Person.prototype.name = ‘tt’;
Person.prototype.age = 18;
Person.prototype.sayHi = function() {
alert(‘Hi’);
}
var person = new Person();
var person1 = new Person();
person.name = ‘oo’;
person.name // oo
person.age // 18
perosn.sayHi() // Hi
person1.age // 18
person1.sayHi() // Hi
从这段代码我们不难看出:

实例可以共享原型上面的属性和方法
实例自身的属性会屏蔽原型上面的同名属性,实例上面没有的属性会去原型上面找

既然原型也是对象,那我们可不可以重写这个对象呢?答案是肯定的
function Person() {}
Person.prototype = {
name: ‘tt’,
age: 18,
sayHi() {
console.log(‘Hi’);
}
}

var person = new Person()
只是当我们在重写原型链的时候需要注意以下的问题:
function Person(){}
var person = new Person();
Person.prototype = {
name: ‘tt’,
age: 18
}

Person.prototype.constructor == Person // false

person.name // undefined
一图胜过千言万语

在已经创建了实例的情况下重写原型,会切断现有实例与新原型之间的联系
重写原型对象,会导致原型对象的 constructor 属性指向 Object,导致原型链关系混乱,所以我们应该在重写原型对象的时候指定 constructor(instanceof 仍然会返回正确的值)

Person.prototype = {
constructor: Person
}
注意:以这种方式重设 constructor 属性会导致它的 Enumerable 特性被设置成 true(默认为 false)
既然现在我们知道了什么是 prototype(原型) 以及它的特点,那么原型链又是什么呢?
原型链

JavaScript 中所有的对象都是由它的原型对象继承而来。而原型对象自身也是一个对象,它也有自己的原型对象,这样层层上溯,就形成了一个类似链表的结构,这就是原型链
同样的,我们使用一张图来描述

所有原型链的终点都是 Object 函数的 prototype 属性

Objec.prototype 指向的原型对象同样拥有原型,不过它的原型是 null,而 null 则没有原型

清楚了原型链的概念,我们就能更清楚地知道属性的查找规则,比如前面的 person 实例属性. 如果自身和原型链上都不存在这个属性,那么属性最终的值就是 undefined,如果是方法就会抛出错误
class 类

ES6 提供了 Class(类) 这个概念,作为对象的模板,通过 class 关键字,可以定义类
为什么会提到 class:
ES6 的 class 可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的 class 写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}

toString() {
return ‘(‘ + this.x + ‘, ‘ + this.y + ‘)’;
}
}

// 可以这么改写
function Point(x, y) {
this.x = x;
this.y = y;
}

Point.prototype.toString = function () {
return ‘(‘ + this.x + ‘, ‘ + this.y + ‘)’;
};
class 里面定义的方法,其实都是定义在构造函数的原型上面实现实例共享, 属性定义在构造函数中,所以 ES6 中的类完全可以看作构造函数的另一种写法
除去 class 类中的一些行为可能与 ES5 存在一些不同,本质上都是通过原型、原型链去定义方法、实现共享。所以,还是文章开始那句话 JavaScript 是基于原型的
更多 class 问题,参考这里
关系判断
instanceof
最常用的确定原型指向关系的关键字,检测的是原型, 但是只能用来判断两个对象是否属于实例关系,而不能判断一个对象实例具体属于哪种类型
function Person(){}
var person = new Person();

person instanceof Person // true
person instanceof Object // true
hasOwnProperty
通过使用 hasOwnProperty 可以确定访问的属性是来自于实例还是原型对象
function Person() {}
Person.prototype = {
name: ‘tt’
}
var person = new Person();
person.age = 15;

person.hasOwnProperty(‘age’) // true
person.hasOwnProperty(‘name’) // false
原型链的问题
由于原型链的存在,我们可以让很多实例去共享原型上面的方法和属性,方便了我们的很多操作。但是原型链并非是十分完美的
function Person(){}
Person.prototype.arr = [1, 2, 3, 4];

var person1 = new Person();
var person2 = new Person();

person1.arr.push(5)
person2.arr // [1, 2, 3, 4, 5]
引用类型,变量保存的就是一个内存中的一个指针。所以,当原型上面的属性是一个引用类型的值时,我们通过其中某一个实例对原型属性的更改,结果会反映在所有实例上面,这也是原型 共享 属性造成的最大问题
另一个问题就是我们在创建子类型(比如上面的 person)时,没有办法向超类型(Person)的构造函数中传递参数
后记
鉴于原型的特点和存在的问题,所以我们在实际开发中一般不会单独使用原型链。一般会使用构造函数和原型相配合的模式,当然这也就牵扯出了 JavaScript 中另一个有意思的领域:继承
那么,什么又是继承呢
且听下回分解
最后,推荐一波前端学习历程,不定期分享一些前端问题和有意思的东西欢迎 star 关注 传送门
参考文档
JavaScript 高级程序设计
ECMAScript6 入门

正文完
 0